From 2f95f4d46ecfdc1d458ca6262266062b62d87ed9 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 4 Nov 2024 16:04:18 -0800 Subject: [PATCH] Add a debug flag to validate schema reflection (#7958) When the flag is set, after every DDL command we reintrospect the database and compare that to the in-meory schema. I've recently landed a few PRs fixing issues exposed by this. (IIRC, I also fixed a few earlier this year when I first wrote this PR.) Overall the system is pretty solid. Run it in CI once a day, since I think it is too slow to run all the time. Fixes #5169. --- .github/Makefile | 6 +- .../tests-reflection.targets.yml | 1 + .../workflows.src/tests-reflection.tpl.yml | 59 ++ .github/workflows/tests-reflection.yml | 519 ++++++++++++++++++ edb/common/debug.py | 3 + edb/server/compiler/compiler.py | 24 + edb/server/compiler/ddl.py | 28 + edb/server/compiler_pool/pool.py | 4 + edb/server/protocol/execute.pyx | 33 ++ edb/server/tenant.py | 15 + 10 files changed, 691 insertions(+), 1 deletion(-) create mode 100644 .github/workflows.src/tests-reflection.targets.yml create mode 100644 .github/workflows.src/tests-reflection.tpl.yml create mode 100644 .github/workflows/tests-reflection.yml diff --git a/.github/Makefile b/.github/Makefile index 9090781f58e..e8475191706 100644 --- a/.github/Makefile +++ b/.github/Makefile @@ -13,7 +13,8 @@ all: workflows/nightly.yml \ workflows/tests-ha.yml \ workflows/tests-pg-versions.yml \ workflows/tests-patches.yml \ - workflows/tests-inplace.yml + workflows/tests-inplace.yml \ + workflows/tests-reflection.yml \ workflows/%.yml: workflows.src/%.tpl.yml workflows.src/%.targets.yml workflows.src/build.inc.yml workflows.src/ls-build.inc.yml $(ROOT)/workflows.src/render.py $* $*.targets.yml @@ -38,3 +39,6 @@ workflows.src/tests-patches.tpl.yml: workflows.src/tests.inc.yml workflows.src/tests-inplace.tpl.yml: workflows.src/tests.inc.yml touch $(ROOT)/workflows.src/tests-inplace.tpl.yml + +workflows.src/tests-reflection.tpl.yml: workflows.src/tests.inc.yml + touch $(ROOT)/workflows.src/tests-inplace.tpl.yml diff --git a/.github/workflows.src/tests-reflection.targets.yml b/.github/workflows.src/tests-reflection.targets.yml new file mode 100644 index 00000000000..99d4a714ac7 --- /dev/null +++ b/.github/workflows.src/tests-reflection.targets.yml @@ -0,0 +1 @@ +data: diff --git a/.github/workflows.src/tests-reflection.tpl.yml b/.github/workflows.src/tests-reflection.tpl.yml new file mode 100644 index 00000000000..3cf3a832d5d --- /dev/null +++ b/.github/workflows.src/tests-reflection.tpl.yml @@ -0,0 +1,59 @@ +<% from "tests.inc.yml" import build, calc_cache_key, restore_cache -%> +name: Tests with reflection validation + +on: + schedule: + - cron: "0 3 * * *" + workflow_dispatch: + inputs: {} + push: + branches: + - "REFL-*" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + <%- call build() -%> + + - name: Compute cache keys + env: + GIST_TOKEN: ${{ secrets.CI_BOT_GIST_TOKEN }} + run: | + << calc_cache_key()|indent >> + <%- endcall %> + + test: + needs: build + runs-on: ubuntu-latest + + steps: + <<- restore_cache() >> + + # Run the test + + - name: Test + env: + EDGEDB_TEST_REPEATS: 1 + run: | + edb test -j2 -v + + workflow-notifications: + if: failure() && github.event_name != 'pull_request' + name: Notify in Slack on failures + needs: + - build + - test + runs-on: ubuntu-latest + permissions: + actions: 'read' + steps: + - name: Slack Workflow Notification + uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309 + with: + repo_token: ${{secrets.GITHUB_TOKEN}} + slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}} + name: 'Workflow notifications' + icon_emoji: ':hammer:' + include_jobs: 'on-failure' diff --git a/.github/workflows/tests-reflection.yml b/.github/workflows/tests-reflection.yml new file mode 100644 index 00000000000..54f7f731a0e --- /dev/null +++ b/.github/workflows/tests-reflection.yml @@ -0,0 +1,519 @@ +name: Tests with reflection validation + +on: + schedule: + - cron: "0 3 * * *" + workflow_dispatch: + inputs: {} + push: + branches: + - "REFL-*" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: false + + - uses: actions/checkout@v4 + with: + fetch-depth: 50 + submodules: true + + - name: Set up Python + uses: actions/setup-python@v5 + id: setup-python + with: + python-version: '3.12.2' + cache: 'pip' + cache-dependency-path: | + pyproject.toml + + # The below is technically a lie as we are technically not + # inside a virtual env, but there is really no reason to bother + # actually creating and activating one as below works just fine. + - name: Export $VIRTUAL_ENV + run: | + venv="$(python -c 'import sys; sys.stdout.write(sys.prefix)')" + echo "VIRTUAL_ENV=${venv}" >> $GITHUB_ENV + + - name: Set up uv cache + uses: actions/cache@v4 + with: + path: ~/.cache/uv + key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }} + + - name: Cached requirements.txt + uses: actions/cache@v4 + id: requirements-cache + with: + path: requirements.txt + key: edb-requirements-${{ hashFiles('pyproject.toml') }} + + - name: Compute requirements.txt + if: steps.requirements-cache.outputs.cache-hit != 'true' + run: | + python -m pip install pip-tools + pip-compile --no-strip-extras --all-build-deps \ + --extra test,language-server \ + --output-file requirements.txt pyproject.toml + + - name: Install Python dependencies + run: | + python -c "import sys; print(sys.prefix)" + python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt + + - name: Compute cache keys + env: + GIST_TOKEN: ${{ secrets.CI_BOT_GIST_TOKEN }} + run: | + mkdir -p shared-artifacts + if [ "$(uname)" = "Darwin" ]; then + 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 + else + find /usr/lib -type f -name 'lib*' -printf '%P %s\n' | sort | sha256sum | cut -d ' ' -f1 > shared-artifacts/lib_cache_key.txt + fi + python setup.py -q ci_helper --type cli > shared-artifacts/edgedbcli_git_rev.txt + python setup.py -q ci_helper --type rust >shared-artifacts/rust_cache_key.txt + python setup.py -q ci_helper --type ext >shared-artifacts/ext_cache_key.txt + python setup.py -q ci_helper --type parsers >shared-artifacts/parsers_cache_key.txt + python setup.py -q ci_helper --type postgres >shared-artifacts/postgres_git_rev.txt + python setup.py -q ci_helper --type libpg_query >shared-artifacts/libpg_query_git_rev.txt + echo 'f8cd94309eaccbfba5dea7835b88c78377608a37' >shared-artifacts/stolon_git_rev.txt + python setup.py -q ci_helper --type bootstrap >shared-artifacts/bootstrap_cache_key.txt + echo EDGEDBCLI_GIT_REV=$(cat shared-artifacts/edgedbcli_git_rev.txt) >> $GITHUB_ENV + echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV + echo LIBPG_QUERY_GIT_REV=$(cat shared-artifacts/libpg_query_git_rev.txt) >> $GITHUB_ENV + echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV + echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV + echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV + + - name: Upload shared artifacts + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + with: + name: shared-artifacts + path: shared-artifacts + retention-days: 1 + + # Restore binary cache + + - name: Handle cached EdgeDB CLI binaries + uses: actions/cache@v4 + id: cli-cache + with: + path: build/cli + key: edb-cli-v3-${{ env.EDGEDBCLI_GIT_REV }} + + - name: Handle cached Rust extensions + uses: actions/cache@v4 + id: rust-cache + with: + path: build/rust_extensions + key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }} + restore-keys: | + edb-rust-v4- + + - name: Handle cached Cython extensions + uses: actions/cache@v4 + id: ext-cache + with: + path: build/extensions + key: edb-ext-v5-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }} + + - name: Handle cached PostgreSQL build + uses: actions/cache@v4 + id: postgres-cache + with: + path: build/postgres/install + key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }} + + - name: Handle cached Stolon build + uses: actions/cache@v4 + id: stolon-cache + with: + path: build/stolon/bin + key: edb-stolon-v2-${{ env.STOLON_GIT_REV }} + + - name: Handle cached libpg_query build + uses: actions/cache@v4 + id: libpg-query-cache + with: + path: edb/pgsql/parser/libpg_query/libpg_query.a + key: edb-libpg_query-v1-${{ env.LIBPG_QUERY_GIT_REV }} + + # Install system dependencies for building + + - name: Install system deps + if: | + steps.cli-cache.outputs.cache-hit != 'true' || + steps.rust-cache.outputs.cache-hit != 'true' || + steps.ext-cache.outputs.cache-hit != 'true' || + steps.stolon-cache.outputs.cache-hit != 'true' || + steps.postgres-cache.outputs.cache-hit != 'true' + run: | + sudo apt-get update + sudo apt-get install -y uuid-dev libreadline-dev bison flex + + - name: Install Rust toolchain + if: | + steps.cli-cache.outputs.cache-hit != 'true' || + steps.rust-cache.outputs.cache-hit != 'true' + uses: dsherret/rust-toolchain-file@v1 + + # Build EdgeDB CLI + + - name: Handle EdgeDB CLI build cache + uses: actions/cache@v4 + if: steps.cli-cache.outputs.cache-hit != 'true' + with: + path: ${{ env.BUILD_TEMP }}/rust/cli + key: edb-cli-build-v7-${{ env.EDGEDBCLI_GIT_REV }} + restore-keys: | + edb-cli-build-v7- + + - name: Build EdgeDB CLI + env: + CARGO_HOME: ${{ env.BUILD_TEMP }}/rust/cli/cargo_home + CACHE_HIT: ${{ steps.cli-cache.outputs.cache-hit }} + run: | + if [[ "$CACHE_HIT" == "true" ]]; then + cp -v build/cli/bin/edgedb edb/cli/edgedb + else + python setup.py -v build_cli + fi + + # Build Rust extensions + + - name: Handle Rust extensions build cache + uses: actions/cache@v4 + if: steps.rust-cache.outputs.cache-hit != 'true' + with: + path: ${{ env.BUILD_TEMP }}/rust/extensions + key: edb-rust-build-v1-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }} + restore-keys: | + edb-rust-build-v1- + + - name: Build Rust extensions + env: + CARGO_HOME: ${{ env.BUILD_TEMP }}/rust/extensions/cargo_home + CACHE_HIT: ${{ steps.rust-cache.outputs.cache-hit }} + run: | + if [[ "$CACHE_HIT" != "true" ]]; then + rm -rf ${BUILD_LIB} + mkdir -p build/rust_extensions + rsync -av ./build/rust_extensions/ ${BUILD_LIB}/ + python setup.py -v build_rust + rsync -av ${BUILD_LIB}/ build/rust_extensions/ + rm -rf ${BUILD_LIB} + fi + rsync -av ./build/rust_extensions/edb/ ./edb/ + + # Build libpg_query + + - name: Build libpg_query + if: | + steps.libpg-query-cache.outputs.cache-hit != 'true' && + steps.ext-cache.outputs.cache-hit != 'true' + run: | + python setup.py build_libpg_query + + # Build extensions + + - name: Handle Cython extensions build cache + uses: actions/cache@v4 + if: steps.ext-cache.outputs.cache-hit != 'true' + with: + path: ${{ env.BUILD_TEMP }}/edb + key: edb-ext-build-v3-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }} + + - name: Build Cython extensions + env: + CACHE_HIT: ${{ steps.ext-cache.outputs.cache-hit }} + BUILD_EXT_MODE: py-only + run: | + if [[ "$CACHE_HIT" != "true" ]]; then + rm -rf ${BUILD_LIB} + mkdir -p ./build/extensions + rsync -av ./build/extensions/ ${BUILD_LIB}/ + BUILD_EXT_MODE=py-only python setup.py -v build_ext + rsync -av ${BUILD_LIB}/ ./build/extensions/ + rm -rf ${BUILD_LIB} + fi + rsync -av ./build/extensions/edb/ ./edb/ + + # Build parsers + + - name: Handle compiled parsers cache + uses: actions/cache@v4 + id: parsers-cache + with: + path: build/lib + key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }} + restore-keys: | + edb-parsers-v3- + + - name: Build parsers + env: + CACHE_HIT: ${{ steps.parsers-cache.outputs.cache-hit }} + run: | + if [[ "$CACHE_HIT" != "true" ]]; then + rm -rf ${BUILD_LIB} + mkdir -p ./build/lib + rsync -av ./build/lib/ ${BUILD_LIB}/ + python setup.py -v build_parsers + rsync -av ${BUILD_LIB}/ ./build/lib/ + rm -rf ${BUILD_LIB} + fi + rsync -av ./build/lib/edb/ ./edb/ + + # Build PostgreSQL + + - name: Build PostgreSQL + env: + CACHE_HIT: ${{ steps.postgres-cache.outputs.cache-hit }} + run: | + if [[ "$CACHE_HIT" == "true" ]]; then + cp build/postgres/install/stamp build/postgres/ + else + python setup.py build_postgres + cp build/postgres/stamp build/postgres/install/ + fi + + # Build Stolon + + - name: Set up Go + if: steps.stolon-cache.outputs.cache-hit != 'true' + uses: actions/setup-go@v2 + with: + go-version: 1.16 + + - uses: actions/checkout@v4 + if: steps.stolon-cache.outputs.cache-hit != 'true' + with: + repository: edgedb/stolon + path: build/stolon + ref: ${{ env.STOLON_GIT_REV }} + fetch-depth: 0 + submodules: false + + - name: Build Stolon + if: steps.stolon-cache.outputs.cache-hit != 'true' + run: | + mkdir -p build/stolon/bin/ + curl -fsSL https://releases.hashicorp.com/consul/1.10.1/consul_1.10.1_linux_amd64.zip | zcat > build/stolon/bin/consul + chmod +x build/stolon/bin/consul + cd build/stolon && make + + # Install edgedb-server and populate egg-info + + - name: Install edgedb-server + env: + BUILD_EXT_MODE: skip + run: | + # --no-build-isolation because we have explicitly installed all deps + # and don't want them to be reinstalled in an "isolated env". + pip install --no-build-isolation --no-deps -e .[test,docs] + + # Refresh the bootstrap cache + + - name: Handle bootstrap cache + uses: actions/cache@v4 + id: bootstrap-cache + with: + path: build/cache + key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }} + restore-keys: | + edb-bootstrap-v2- + + - name: Bootstrap EdgeDB Server + if: steps.bootstrap-cache.outputs.cache-hit != 'true' + run: | + edb server --bootstrap-only + + test: + needs: build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: false + + - uses: actions/checkout@v4 + with: + fetch-depth: 50 + submodules: true + + - name: Set up Python + uses: actions/setup-python@v5 + id: setup-python + with: + python-version: '3.12.2' + cache: 'pip' + cache-dependency-path: | + pyproject.toml + + # The below is technically a lie as we are technically not + # inside a virtual env, but there is really no reason to bother + # actually creating and activating one as below works just fine. + - name: Export $VIRTUAL_ENV + run: | + venv="$(python -c 'import sys; sys.stdout.write(sys.prefix)')" + echo "VIRTUAL_ENV=${venv}" >> $GITHUB_ENV + + - name: Set up uv cache + uses: actions/cache@v4 + with: + path: ~/.cache/uv + key: uv-cache-${{ runner.os }}-py-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }} + + - name: Download requirements.txt + uses: actions/cache@v4 + with: + path: requirements.txt + key: edb-requirements-${{ hashFiles('pyproject.toml') }} + + - name: Install Python dependencies + run: python -m pip install uv~=0.1.0 && uv pip install -U -r requirements.txt + + # Restore the artifacts and environment variables + + - name: Download shared artifacts + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: shared-artifacts + path: shared-artifacts + + - name: Set environment variables + run: | + echo EDGEDBCLI_GIT_REV=$(cat shared-artifacts/edgedbcli_git_rev.txt) >> $GITHUB_ENV + echo POSTGRES_GIT_REV=$(cat shared-artifacts/postgres_git_rev.txt) >> $GITHUB_ENV + echo STOLON_GIT_REV=$(cat shared-artifacts/stolon_git_rev.txt) >> $GITHUB_ENV + echo BUILD_LIB=$(python setup.py -q ci_helper --type build_lib) >> $GITHUB_ENV + echo BUILD_TEMP=$(python setup.py -q ci_helper --type build_temp) >> $GITHUB_ENV + + # Restore build cache + + - name: Restore cached EdgeDB CLI binaries + uses: actions/cache@v4 + id: cli-cache + with: + path: build/cli + key: edb-cli-v3-${{ env.EDGEDBCLI_GIT_REV }} + + - name: Restore cached Rust extensions + uses: actions/cache@v4 + id: rust-cache + with: + path: build/rust_extensions + key: edb-rust-v4-${{ hashFiles('shared-artifacts/rust_cache_key.txt') }} + + - name: Restore cached Cython extensions + uses: actions/cache@v4 + id: ext-cache + with: + path: build/extensions + key: edb-ext-v5-${{ hashFiles('shared-artifacts/ext_cache_key.txt') }} + + - name: Restore compiled parsers cache + uses: actions/cache@v4 + id: parsers-cache + with: + path: build/lib + key: edb-parsers-v3-${{ hashFiles('shared-artifacts/parsers_cache_key.txt') }} + + - name: Restore cached PostgreSQL build + uses: actions/cache@v4 + id: postgres-cache + with: + path: build/postgres/install + key: edb-postgres-v3-${{ env.POSTGRES_GIT_REV }}-${{ hashFiles('shared-artifacts/lib_cache_key.txt') }} + + - name: Restore cached Stolon build + uses: actions/cache@v4 + id: stolon-cache + with: + path: build/stolon/bin + key: edb-stolon-v2-${{ env.STOLON_GIT_REV }} + + - name: Restore bootstrap cache + uses: actions/cache@v4 + id: bootstrap-cache + with: + path: build/cache + key: edb-bootstrap-v2-${{ hashFiles('shared-artifacts/bootstrap_cache_key.txt') }} + + - name: Stop if we cannot retrieve the cache + if: | + steps.cli-cache.outputs.cache-hit != 'true' || + steps.rust-cache.outputs.cache-hit != 'true' || + steps.ext-cache.outputs.cache-hit != 'true' || + steps.parsers-cache.outputs.cache-hit != 'true' || + steps.postgres-cache.outputs.cache-hit != 'true' || + steps.stolon-cache.outputs.cache-hit != 'true' || + steps.bootstrap-cache.outputs.cache-hit != 'true' + run: | + echo ::error::Cannot retrieve build cache. + exit 1 + + - name: Validate cached binaries + run: | + # Validate EdgeDB CLI + ./build/cli/bin/edgedb --version || exit 1 + + # Validate Stolon + ./build/stolon/bin/stolon-sentinel --version || exit 1 + ./build/stolon/bin/stolon-keeper --version || exit 1 + ./build/stolon/bin/stolon-proxy --version || exit 1 + + # Validate PostgreSQL + ./build/postgres/install/bin/postgres --version || exit 1 + ./build/postgres/install/bin/pg_config --version || exit 1 + + - name: Restore cache into the source tree + run: | + cp -v build/cli/bin/edgedb edb/cli/edgedb + rsync -av ./build/rust_extensions/edb/ ./edb/ + rsync -av ./build/extensions/edb/ ./edb/ + rsync -av ./build/lib/edb/ ./edb/ + cp build/postgres/install/stamp build/postgres/ + + - name: Install edgedb-server + env: + BUILD_EXT_MODE: skip + run: | + # --no-build-isolation because we have explicitly installed all deps + # and don't want them to be reinstalled in an "isolated env". + pip install --no-build-isolation --no-deps -e .[test,docs] + + # Run the test + + - name: Test + env: + EDGEDB_TEST_REPEATS: 1 + run: | + edb test -j2 -v + + workflow-notifications: + if: failure() && github.event_name != 'pull_request' + name: Notify in Slack on failures + needs: + - build + - test + runs-on: ubuntu-latest + permissions: + actions: 'read' + steps: + - name: Slack Workflow Notification + uses: Gamesight/slack-workflow-status@26a36836c887f260477432e4314ec3490a84f309 + with: + repo_token: ${{secrets.GITHUB_TOKEN}} + slack_webhook_url: ${{secrets.ACTIONS_SLACK_WEBHOOK_URL}} + name: 'Workflow notifications' + icon_emoji: ':hammer:' + include_jobs: 'on-failure' diff --git a/edb/common/debug.py b/edb/common/debug.py index 5b719181497..737d6f7a380 100644 --- a/edb/common/debug.py +++ b/edb/common/debug.py @@ -134,6 +134,9 @@ class flags(metaclass=FlagsMeta): delta_execute_ddl = Flag( doc="Output just the DDL commands as executed during migration.") + delta_validate_reflection = Flag( + doc="Whether to do expensive validation of reflection correctness.") + server = Flag( doc="Print server errors.") diff --git a/edb/server/compiler/compiler.py b/edb/server/compiler/compiler.py index 4f922a5c5ce..5813181485c 100644 --- a/edb/server/compiler/compiler.py +++ b/edb/server/compiler/compiler.py @@ -1170,6 +1170,30 @@ def analyze_explain_output( return explain.analyze_explain_output( query_asts_pickled, data, self.state.std_schema) + def validate_schema_equivalence( + self, + schema_a: bytes, + schema_b: bytes, + global_schema: bytes, + conn_state_pickle: Any, + ) -> None: + if conn_state_pickle: + conn_state = pickle.loads(conn_state_pickle) + if ( + conn_state + and ( + conn_state.current_tx().get_migration_state() + or conn_state.current_tx().get_migration_rewrite_state() + ) + ): + return + ddl.validate_schema_equivalence( + self.state, + pickle.loads(schema_a), + pickle.loads(schema_b), + pickle.loads(global_schema), + ) + def compile_schema_storage_in_delta( ctx: CompileContext, diff --git a/edb/server/compiler/ddl.py b/edb/server/compiler/ddl.py index 6a1d7f20b80..9754e9eeeb8 100644 --- a/edb/server/compiler/ddl.py +++ b/edb/server/compiler/ddl.py @@ -1700,3 +1700,31 @@ def administer_prepare_upgrade( cacheable=False, migration_block_query=True, ) + + +def validate_schema_equivalence( + state: compiler.CompilerState, + schema_a: s_schema.FlatSchema, + schema_b: s_schema.FlatSchema, + global_schema: s_schema.FlatSchema, +) -> None: + schema_a_full = s_schema.ChainedSchema( + state.std_schema, + schema_a, + global_schema, + ) + schema_b_full = s_schema.ChainedSchema( + state.std_schema, + schema_b, + global_schema, + ) + + diff = s_ddl.delta_schemas(schema_a_full, schema_b_full) + complete = not bool(diff.get_subcommands()) + if not complete: + if debug.flags.delta_plan: + debug.header('COMPARE SCHEMAS MISMATCH') + debug.dump(diff) + raise AssertionError( + f'schemas did not match after introspection:\n{debug.dumps(diff)}' + ) diff --git a/edb/server/compiler_pool/pool.py b/edb/server/compiler_pool/pool.py index 95c7f290c25..97820b3fe1e 100644 --- a/edb/server/compiler_pool/pool.py +++ b/edb/server/compiler_pool/pool.py @@ -603,6 +603,10 @@ async def analyze_explain_output(self, *args, **kwargs): return await self._simple_call( 'analyze_explain_output', *args, **kwargs) + async def validate_schema_equivalence(self, *args, **kwargs): + return await self._simple_call( + 'validate_schema_equivalence', *args, **kwargs) + def get_debug_info(self): return {} diff --git a/edb/server/protocol/execute.pyx b/edb/server/protocol/execute.pyx index 4760e4bb341..c27858665f1 100644 --- a/edb/server/protocol/execute.pyx +++ b/edb/server/protocol/execute.pyx @@ -358,6 +358,20 @@ async def execute( if config_ops: await dbv.apply_config_ops(be_conn, config_ops) + if query_unit.user_schema and debug.flags.delta_validate_reflection: + global_schema = ( + query_unit.global_schema or dbv.get_global_schema_pickle()) + new_user_schema = await dbv.tenant._debug_introspect( + be_conn, global_schema) + compiler_pool = dbv.server.get_compiler_pool() + await compiler_pool.validate_schema_equivalence( + query_unit.user_schema, + new_user_schema, + global_schema, + dbv._last_comp_state, + ) + query_unit.user_schema = new_user_schema + except Exception as ex: # If we made schema changes, include the new schema in the # exception so that it can be used when interpreting. @@ -571,6 +585,22 @@ async def execute_script( raise else: + updated_user_schema = False + if user_schema and debug.flags.delta_validate_reflection: + cur_global_schema = ( + global_schema or dbv.get_global_schema_pickle()) + new_user_schema = await dbv.tenant._debug_introspect( + conn, cur_global_schema) + compiler_pool = dbv.server.get_compiler_pool() + await compiler_pool.validate_schema_equivalence( + user_schema, + new_user_schema, + cur_global_schema, + dbv._last_comp_state, + ) + user_schema = new_user_schema + updated_user_schema = True + if not in_tx: side_effects = dbv.commit_implicit_tx( user_schema, @@ -586,6 +616,9 @@ async def execute_script( state = dbv.serialize_state() if state is not orig_state: conn.last_state = state + elif updated_user_schema: + dbv._in_tx_user_schema_pickle = user_schema + if unit_group.state_serializer is not None: dbv.set_state_serializer(unit_group.state_serializer) diff --git a/edb/server/tenant.py b/edb/server/tenant.py index 7b7b0a9a9a1..187f3d10b00 100644 --- a/edb/server/tenant.py +++ b/edb/server/tenant.py @@ -1101,6 +1101,21 @@ async def _introspect_extensions( return extensions + async def _debug_introspect( + self, + conn: pgcon.PGConnection, + global_schema_pickle, + ) -> Any: + user_schema_json = ( + await self._server.introspect_user_schema_json(conn) + ) + db_config_json = await self._server.introspect_db_config(conn) + + compiler_pool = self._server.get_compiler_pool() + return (await compiler_pool.parse_user_schema_db_config( + user_schema_json, db_config_json, global_schema_pickle, + )).user_schema_pickle + async def introspect_db( self, dbname: str,