From 3335f06159de502b3449236be65ce98aaf48afb0 Mon Sep 17 00:00:00 2001 From: Aryan Godara <65490434+AryanGodara@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:46:24 +0530 Subject: [PATCH] Isolate pool-indexer flyway migrations from the shared DB set --- .devcontainer/setup-e2e.sh | 13 +++ .github/workflows/pull-request.yaml | 1 + crates/database/src/lib.rs | 9 +- crates/e2e/tests/e2e/pool_indexer.rs | 17 ++-- database/README.md | 78 +-------------- database/sql-pool-indexer/README.md | 99 +++++++++++++++++++ .../V110__pool_indexer_uniswap_v3.sql | 59 +++++++++++ .../V111__drop_pool_indexer_uniswap_v3.sql | 13 +++ docker-compose.yaml | 37 +++++++ 9 files changed, 236 insertions(+), 90 deletions(-) create mode 100644 database/sql-pool-indexer/README.md create mode 100644 database/sql-pool-indexer/V110__pool_indexer_uniswap_v3.sql create mode 100644 database/sql/V111__drop_pool_indexer_uniswap_v3.sql diff --git a/.devcontainer/setup-e2e.sh b/.devcontainer/setup-e2e.sh index 044740f317..f72f020b4d 100755 --- a/.devcontainer/setup-e2e.sh +++ b/.devcontainer/setup-e2e.sh @@ -66,4 +66,17 @@ docker run --rm --network=host \ -url="jdbc:postgresql://127.0.0.1:${PGPORT}/${PGDATABASE}?user=${PGUSER}&password=" \ migrate +# The pool-indexer has its own database migrated from its own directory (mirrors +# the per-network prod DB), separate from the autopilot/orderbook set above. +psql -h 127.0.0.1 -U postgres -d postgres -tAc \ + "SELECT 1 FROM pg_database WHERE datname='pool_indexer'" | grep -q 1 \ + || psql -h 127.0.0.1 -U postgres -d postgres -c \ + "CREATE DATABASE pool_indexer OWNER \"${PGUSER}\";" +docker run --rm --network=host \ + -v "$REPO_ROOT/database/sql-pool-indexer:/flyway/sql:ro" \ + -v "$REPO_ROOT/database/conf:/flyway/conf:ro" \ + flyway/flyway:10.7.1 \ + -url="jdbc:postgresql://127.0.0.1:${PGPORT}/pool_indexer?user=${PGUSER}&password=" \ + migrate + echo "==> e2e environment ready (postgres)" diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index f0dd558329..c2c2c4bd61 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -167,6 +167,7 @@ jobs: tool: nextest - run: docker compose -f docker-compose.yaml up -d db - run: docker compose -f docker-compose.yaml up migrations --abort-on-container-failure + - run: docker compose -f docker-compose.yaml up migrations-pool-indexer --abort-on-container-failure - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0 with: just-version: 1.39.0 diff --git a/crates/database/src/lib.rs b/crates/database/src/lib.rs index 16257f6849..b47d4f7775 100644 --- a/crates/database/src/lib.rs +++ b/crates/database/src/lib.rs @@ -62,7 +62,6 @@ pub const TABLES: &[&str] = &[ "last_indexed_blocks", "onchain_order_invalidations", "onchain_placed_orders", - "pool_indexer_checkpoints", "presignature_events", "proposed_jit_orders", "quotes", @@ -72,8 +71,6 @@ pub const TABLES: &[&str] = &[ "solver_competitions", "surplus_capturing_jit_order_owners", "trades", - "uniswap_v3_pool_states", - "uniswap_v3_pools", ]; /// The names of potentially big volume tables we use in the db. @@ -88,7 +85,6 @@ pub const LARGE_TABLES: &[&str] = &[ "order_quotes", "proposed_solutions", "proposed_trade_executions", - "uniswap_v3_ticks", ]; pub fn all_tables() -> impl Iterator { @@ -98,9 +94,8 @@ pub fn all_tables() -> impl Iterator { /// Delete all data in the database. Only used by tests. /// /// Truncates all tables in a single statement so Postgres accepts foreign-key -/// cycles between listed tables (e.g. `uniswap_v3_pool_states` → -/// `uniswap_v3_pools`). Individual per-table `TRUNCATE`s error out when any -/// other listed table references the one being truncated. +/// cycles between listed tables. Individual per-table `TRUNCATE`s error out +/// when any other listed table references the one being truncated. #[expect(non_snake_case)] pub async fn clear_DANGER_(ex: &mut PgTransaction<'_>) -> sqlx::Result<()> { let tables = all_tables().collect::>().join(", "); diff --git a/crates/e2e/tests/e2e/pool_indexer.rs b/crates/e2e/tests/e2e/pool_indexer.rs index 86a9c3b84c..6ee3b4c88c 100644 --- a/crates/e2e/tests/e2e/pool_indexer.rs +++ b/crates/e2e/tests/e2e/pool_indexer.rs @@ -131,7 +131,10 @@ sol! { const POOL_INDEXER_PORT: u16 = 7778; const POOL_INDEXER_HOST: &str = "http://127.0.0.1:7778"; const POOL_INDEXER_METRICS_PORT: u16 = 7779; -const LOCAL_DB_URL: &str = "postgresql://"; +// The indexer has its own database (mirrors the per-network prod DB), migrated +// from `database/sql-pool-indexer` by the `migrations-pool-indexer` flyway step +// (docker-compose / setup-e2e.sh), separate from the shared autopilot DB. +const POOL_INDEXER_DB_URL: &str = "postgresql:///pool_indexer"; // sqrt(1) * 2^96 — valid starting price const INITIAL_SQRT_PRICE: u128 = 1u128 << 96; @@ -162,6 +165,8 @@ struct TicksResponse { #[derive(Deserialize)] struct TickEntry {} +/// Truncates the indexer's tables between tests. The schema itself is +/// provisioned by flyway (`migrations-pool-indexer`), so this just clears rows. async fn clear_pool_indexer_tables(db: &PgPool) { sqlx::query( "TRUNCATE uniswap_v3_ticks, uniswap_v3_pool_states, uniswap_v3_pools, \ @@ -190,7 +195,7 @@ async fn seed_checkpoint(db: &PgPool, factory: Address, block: u64) { async fn spawn_pool_indexer(factory: Address, metrics_port: u16) -> tokio::task::JoinHandle<()> { let config = Configuration { database: DatabaseConfig { - url: LOCAL_DB_URL.parse().unwrap(), + url: POOL_INDEXER_DB_URL.parse().unwrap(), max_connections: NonZeroU32::new(5).unwrap(), }, network: NetworkConfig { @@ -390,7 +395,7 @@ async fn driver_integration(web3: Web3) { const POOLS_BY_IDS_ROUTE: &str = "/api/v1/{network}/uniswap/v3/pools/by-ids"; const TICKS_ROUTE: &str = "/api/v1/{network}/uniswap/v3/pools/ticks"; - let db = PgPool::connect(LOCAL_DB_URL).await.unwrap(); + let db = PgPool::connect(POOL_INDEXER_DB_URL).await.unwrap(); clear_pool_indexer_tables(&db).await; let mut onchain = OnchainComponents::deploy(web3.clone()).await; @@ -482,7 +487,7 @@ async fn local_node_pool_indexer_checkpoint_resume() { /// count, sqrt_price / tick / liquidity, and the checkpoint all survive a /// stop+start. async fn checkpoint_resume(web3: Web3) { - let db = PgPool::connect(LOCAL_DB_URL).await.unwrap(); + let db = PgPool::connect(POOL_INDEXER_DB_URL).await.unwrap(); clear_pool_indexer_tables(&db).await; let (factory, pool_addr) = deploy_univ3(&web3).await; @@ -533,7 +538,7 @@ async fn local_node_pool_indexer_api_errors() { /// 400, a valid-but-unknown address must come back as 200 with empty ticks. /// Lets callers distinguish "garbage input" from "no data yet". async fn api_errors(web3: Web3) { - let db = PgPool::connect(LOCAL_DB_URL).await.unwrap(); + let db = PgPool::connect(POOL_INDEXER_DB_URL).await.unwrap(); clear_pool_indexer_tables(&db).await; let (factory, _pool) = deploy_univ3(&web3).await; @@ -585,7 +590,7 @@ async fn local_node_pool_indexer_pagination() { /// every pool exactly once. Three pools is the smallest set that exercises /// a mid-stream cursor and the `next_cursor = null` terminator. async fn pagination(web3: Web3) { - let db = PgPool::connect(LOCAL_DB_URL).await.unwrap(); + let db = PgPool::connect(POOL_INDEXER_DB_URL).await.unwrap(); clear_pool_indexer_tables(&db).await; let (factory, _pool1) = deploy_univ3(&web3).await; diff --git a/database/README.md b/database/README.md index 4a4679f9e5..68e4b3a8fe 100644 --- a/database/README.md +++ b/database/README.md @@ -532,83 +532,7 @@ Indexes: - jit\_user\_order\_creation\_timestamp: btree(`owner`, `creation_timestamp` DESC) - jit\_event\_id: btree(`block_number`, `log_index`) -### pool\_indexer\_checkpoints - -Highest finalized block processed per `contract_address` by `pool-indexer`. `contract_address` is the factory address. The indexer runs one process per network against its own DB, so there's no `chain_id` column. - - Column | Type | Nullable | Details ---------------------|--------|----------|-------- - contract\_address | bytea | not null | Factory address (20 bytes) - block\_number | bigint | not null | - -Indexes: -- PRIMARY KEY: btree (`contract_address`) - -### uniswap\_v3\_pools - -One row per pool discovered from a `PoolCreated` event. `token{0,1}_{decimals,symbol}` are nullable and filled in by the backfill task. `factory` partitions the table when multiple V3-compatible factories run on the same network so each indexer touches only its own rows. - - Column | Type | Nullable | Details --------------------|----------|----------|-------- - address | bytea | not null | Pool address (20 bytes) - factory | bytea | not null | Address of the V3 factory that emitted `PoolCreated` - token0 | bytea | not null | - token1 | bytea | not null | - fee | int | not null | Hundredths of a basis point (500 = 0.05%, 3000 = 0.3%, 10000 = 1%). `CHECK (fee > 0)`. - token0\_decimals | smallint | nullable | `NULL` = not yet fetched. `-1` = sentinel for "fetched but call failed" - token1\_decimals | smallint | nullable | - token0\_symbol | text | nullable | `NULL` = not yet fetched. `""` = sentinel for "fetched but call failed" - token1\_symbol | text | nullable | - created\_block | bigint | not null | Block in which the pool was created on-chain - -Indexes: -- PRIMARY KEY: btree (`address`) -- Four partial indexes on `(token{0,1})` with predicate `token{0,1}_{symbol,decimals} IS NULL` to power the backfill scan. - -### uniswap\_v3\_pool\_states - -Current state per pool: `sqrt_price_x96` and `tick` come from the latest `Swap`/`Initialize`; `liquidity` and `block_number` also update on in-range `Mint`/`Burn`. FK → `uniswap_v3_pools`. - -**Uniswap V3 pool-state primer.** Three values capture a pool's instantaneous state: - -- `sqrt_price_x96` — `sqrt(price) * 2^96` where `price = token1/token0`, stored in Q64.96 fixed-point. The square-root form keeps swap math additive and bounds precision loss over the uint160 range. Mirrors on-chain `slot0.sqrtPriceX96`. -- `tick` — `floor(log_{1.0001}(price))`. Each tick is a ~0.01% price step; the current tick is the bucket the live price falls into. Routers use it to decide which positions are in-range. -- `liquidity` — sum of every position's liquidity whose `tickLower <= current_tick < tickUpper`. This is the `L` in V3's invariant `Δsqrt_price = Δamount / L`. Updates on `Swap` (the event carries the new value) and on `Mint`/`Burn` whose range spans the current tick. - -The per-tick deltas that move `liquidity` when the price crosses a tick boundary live in [`uniswap_v3_ticks`](#uniswap_v3_ticks). - - Column | Type | Nullable | Details --------------------|---------|----------|-------- - pool\_address | bytea | not null | FK → `uniswap_v3_pools(address)` - block\_number | bigint | not null | Block of the most recent state-changing event (`Swap`, `Initialize`, or in-range `Mint`/`Burn`). - sqrt\_price\_x96 | numeric | not null | uint160 — see primer above - liquidity | numeric | not null | uint128 — see primer above - tick | int | not null | signed int24 — see primer above - -Indexes: -- PRIMARY KEY: btree (`pool_address`) - -### uniswap\_v3\_ticks - -Per-tick liquidity deltas. Rows with `liquidity_net = 0` are pruned. FK → `uniswap_v3_pools`. - -**Why deltas instead of per-tick totals.** A V3 position covers `[tickLower, tickUpper)` and contributes to pool liquidity only when the current tick is in that range. We store the entering / exiting deltas at the bounds: - -- At `tickLower`: `liquidity_net += position.liquidity` (entering) -- At `tickUpper`: `liquidity_net -= position.liquidity` (exiting) - -When a swap crosses a tick boundary, the pool's `liquidity` shifts by `± tick.liquidity_net`. This encoding makes the per-tick aggregate O(1) at swap time — no per-position iteration. - -Quoters consult these to predict liquidity changes at tick crossings during swap simulation. Without them, large swaps would be priced as if the liquidity stayed flat, producing wildy wrong quotes - - Column | Type | Nullable | Details -----------------|---------|----------|-------- - pool\_address | bytea | not null | FK → `uniswap_v3_pools(address)` - tick\_idx | int | not null | Tick coordinate (signed int24); same domain as [`uniswap_v3_pool_states.tick`](#uniswap_v3_pool_states) - liquidity\_net | numeric | not null | int128, signed — net liquidity entering (+) / exiting (-) at this tick - -Indexes: -- PRIMARY KEY: btree (`pool_address`, `tick_idx`) +The `pool-indexer` service uses its own per-network database, not these shared DBs. Its tables (`pool_indexer_checkpoints`, `uniswap_v3_pools`, `uniswap_v3_pool_states`, `uniswap_v3_ticks`) and migrations live in [`sql-pool-indexer/`](sql-pool-indexer/). ### cow\_amms diff --git a/database/sql-pool-indexer/README.md b/database/sql-pool-indexer/README.md new file mode 100644 index 0000000000..fec9aa2f60 --- /dev/null +++ b/database/sql-pool-indexer/README.md @@ -0,0 +1,99 @@ +# Pool-indexer migrations + +Flyway migrations for the pool-indexer's own per-network database (e.g. +`ink_pool_indexer`), kept out of the shared `../sql/` set so they don't run +against the autopilot/orderbook main DBs. + +The migration image ships both dirs; init containers pick one via `-locations`: + +| DB | location | +|---------------------|-----------------------------------------------------| +| autopilot/orderbook | `/flyway/sql` (default) | +| pool-indexer | `-locations=filesystem:/flyway/sql-pool-indexer` | + +New pool-indexer migrations go here, never in `../sql/`. `V110` is duplicated +from `../sql/` on purpose: the shared copy can't be deleted (Flyway checksums +applied migrations) so it's cancelled there by `../sql/V111`. + +## Schema + +The tables below live in the indexer's own per-network database (e.g. +`ink_pool_indexer`), created by the migrations in this directory. + +### pool\_indexer\_checkpoints + +Highest finalized block processed per `contract_address` by `pool-indexer`. `contract_address` is the factory address. The indexer runs one process per network against its own DB, so there's no `chain_id` column. + + Column | Type | Nullable | Details +--------------------|--------|----------|-------- + contract\_address | bytea | not null | Factory address (20 bytes) + block\_number | bigint | not null | + +Indexes: +- PRIMARY KEY: btree (`contract_address`) + +### uniswap\_v3\_pools + +One row per pool discovered from a `PoolCreated` event. `token{0,1}_{decimals,symbol}` are nullable and filled in by the backfill task. `factory` partitions the table when multiple V3-compatible factories run on the same network so each indexer touches only its own rows. + + Column | Type | Nullable | Details +-------------------|----------|----------|-------- + address | bytea | not null | Pool address (20 bytes) + factory | bytea | not null | Address of the V3 factory that emitted `PoolCreated` + token0 | bytea | not null | + token1 | bytea | not null | + fee | int | not null | Hundredths of a basis point (500 = 0.05%, 3000 = 0.3%, 10000 = 1%). `CHECK (fee > 0)`. + token0\_decimals | smallint | nullable | `NULL` = not yet fetched. `-1` = sentinel for "fetched but call failed" + token1\_decimals | smallint | nullable | + token0\_symbol | text | nullable | `NULL` = not yet fetched. `""` = sentinel for "fetched but call failed" + token1\_symbol | text | nullable | + created\_block | bigint | not null | Block in which the pool was created on-chain + +Indexes: +- PRIMARY KEY: btree (`address`) +- Four partial indexes on `(token{0,1})` with predicate `token{0,1}_{symbol,decimals} IS NULL` to power the backfill scan. + +### uniswap\_v3\_pool\_states + +Current state per pool: `sqrt_price_x96` and `tick` come from the latest `Swap`/`Initialize`; `liquidity` and `block_number` also update on in-range `Mint`/`Burn`. FK → `uniswap_v3_pools`. + +**Uniswap V3 pool-state primer.** Three values capture a pool's instantaneous state: + +- `sqrt_price_x96` — `sqrt(price) * 2^96` where `price = token1/token0`, stored in Q64.96 fixed-point. The square-root form keeps swap math additive and bounds precision loss over the uint160 range. Mirrors on-chain `slot0.sqrtPriceX96`. +- `tick` — `floor(log_{1.0001}(price))`. Each tick is a ~0.01% price step; the current tick is the bucket the live price falls into. Routers use it to decide which positions are in-range. +- `liquidity` — sum of every position's liquidity whose `tickLower <= current_tick < tickUpper`. This is the `L` in V3's invariant `Δsqrt_price = Δamount / L`. Updates on `Swap` (the event carries the new value) and on `Mint`/`Burn` whose range spans the current tick. + +The per-tick deltas that move `liquidity` when the price crosses a tick boundary live in [`uniswap_v3_ticks`](#uniswap_v3_ticks). + + Column | Type | Nullable | Details +-------------------|---------|----------|-------- + pool\_address | bytea | not null | FK → `uniswap_v3_pools(address)` + block\_number | bigint | not null | Block of the most recent state-changing event (`Swap`, `Initialize`, or in-range `Mint`/`Burn`). + sqrt\_price\_x96 | numeric | not null | uint160 — see primer above + liquidity | numeric | not null | uint128 — see primer above + tick | int | not null | signed int24 — see primer above + +Indexes: +- PRIMARY KEY: btree (`pool_address`) + +### uniswap\_v3\_ticks + +Per-tick liquidity deltas. Rows with `liquidity_net = 0` are pruned. FK → `uniswap_v3_pools`. + +**Why deltas instead of per-tick totals.** A V3 position covers `[tickLower, tickUpper)` and contributes to pool liquidity only when the current tick is in that range. We store the entering / exiting deltas at the bounds: + +- At `tickLower`: `liquidity_net += position.liquidity` (entering) +- At `tickUpper`: `liquidity_net -= position.liquidity` (exiting) + +When a swap crosses a tick boundary, the pool's `liquidity` shifts by `± tick.liquidity_net`. This encoding makes the per-tick aggregate O(1) at swap time — no per-position iteration. + +Quoters consult these to predict liquidity changes at tick crossings during swap simulation. Without them, large swaps would be priced as if the liquidity stayed flat, producing wildy wrong quotes + + Column | Type | Nullable | Details +----------------|---------|----------|-------- + pool\_address | bytea | not null | FK → `uniswap_v3_pools(address)` + tick\_idx | int | not null | Tick coordinate (signed int24); same domain as [`uniswap_v3_pool_states.tick`](#uniswap_v3_pool_states) + liquidity\_net | numeric | not null | int128, signed — net liquidity entering (+) / exiting (-) at this tick + +Indexes: +- PRIMARY KEY: btree (`pool_address`, `tick_idx`) diff --git a/database/sql-pool-indexer/V110__pool_indexer_uniswap_v3.sql b/database/sql-pool-indexer/V110__pool_indexer_uniswap_v3.sql new file mode 100644 index 0000000000..0993d80e1d --- /dev/null +++ b/database/sql-pool-indexer/V110__pool_indexer_uniswap_v3.sql @@ -0,0 +1,59 @@ +-- Tracks the highest finalized block fully processed per factory contract. +-- A DB instance hosts a single network, so no `chain_id` column is needed. +CREATE TABLE pool_indexer_checkpoints ( + contract_address BYTEA NOT NULL, -- factory address + block_number BIGINT NOT NULL, + PRIMARY KEY (contract_address) +); + +-- One row per pool, discovered from `PoolCreated` events. `factory` +-- partitions the table so multiple V3-compatible factories on the same +-- network can coexist (logs are fetched chain-wide, then partitioned at +-- the write boundary). +CREATE TABLE uniswap_v3_pools ( + address BYTEA NOT NULL, -- pool address + factory BYTEA NOT NULL, + token0 BYTEA NOT NULL, + token1 BYTEA NOT NULL, + fee INT NOT NULL CHECK (fee > 0), -- hundredths of a basis point (500 = 0.05%, 3000 = 0.3%, 10000 = 1%) + token0_decimals SMALLINT, + token1_decimals SMALLINT, + token0_symbol TEXT, + token1_symbol TEXT, + created_block BIGINT NOT NULL, + PRIMARY KEY (address) +); + +-- Current state per pool. `sqrt_price_x96` + `tick` come from the latest +-- Swap/Initialize; `liquidity` + `block_number` also update on in-range +-- Mint/Burn events. +CREATE TABLE uniswap_v3_pool_states ( + pool_address BYTEA NOT NULL, + block_number BIGINT NOT NULL, + sqrt_price_x96 NUMERIC NOT NULL, -- uint160 + liquidity NUMERIC NOT NULL, -- uint128 + -- `tick` here means the Uniswap V3 *price tick index* (signed + -- int24), not a database index. See `uniswap_v3_ticks.tick_idx`. + tick INT NOT NULL, + PRIMARY KEY (pool_address), + FOREIGN KEY (pool_address) REFERENCES uniswap_v3_pools(address) +); + +-- Active ticks per pool. Rows with `liquidity_net = 0` are pruned. +-- `tick_idx` is the price tick coordinate (signed int24) — same domain +-- as `uniswap_v3_pool_states.tick`, one row per active tick boundary. +CREATE TABLE uniswap_v3_ticks ( + pool_address BYTEA NOT NULL, + tick_idx INT NOT NULL, + liquidity_net NUMERIC NOT NULL, -- int128 (can be negative) + PRIMARY KEY (pool_address, tick_idx), + FOREIGN KEY (pool_address) REFERENCES uniswap_v3_pools(address) +); + +-- Symbol/decimals backfill hot paths. Partial on `IS NULL` so each +-- index shrinks to near-empty once most rows are populated (real value +-- or the `""` / `-1` "tried, failed" sentinel). +CREATE INDEX ON uniswap_v3_pools (token0) WHERE token0_symbol IS NULL; +CREATE INDEX ON uniswap_v3_pools (token1) WHERE token1_symbol IS NULL; +CREATE INDEX ON uniswap_v3_pools (token0) WHERE token0_decimals IS NULL; +CREATE INDEX ON uniswap_v3_pools (token1) WHERE token1_decimals IS NULL; diff --git a/database/sql/V111__drop_pool_indexer_uniswap_v3.sql b/database/sql/V111__drop_pool_indexer_uniswap_v3.sql new file mode 100644 index 0000000000..5dec00625c --- /dev/null +++ b/database/sql/V111__drop_pool_indexer_uniswap_v3.sql @@ -0,0 +1,13 @@ +-- Removes the pool-indexer tables from databases that run this shared +-- migration set. The indexer's schema (V110) was checked in here by mistake, +-- so every DB's flyway run created these tables even though only the dedicated +-- `*_pool_indexer` DB ever uses them. +-- +-- The indexer's own DB applies its schema from a separate migration location +-- (see `database/sql-pool-indexer/`) and never runs this. +-- Forward-only cleanup: V110 is left untouched so its checksum stays valid on +-- DBs that already applied it. Children (FK to uniswap_v3_pools) drop first. +DROP TABLE IF EXISTS uniswap_v3_pool_states; +DROP TABLE IF EXISTS uniswap_v3_ticks; +DROP TABLE IF EXISTS uniswap_v3_pools; +DROP TABLE IF EXISTS pool_indexer_checkpoints; diff --git a/docker-compose.yaml b/docker-compose.yaml index 75b479a2a5..b2fdf5544a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -36,5 +36,42 @@ services: source: ./database/conf/ target: /flyway/conf + # Flyway can't create databases, so provision the pool-indexer's own DB first. + # Idempotent (mirrors .devcontainer/setup-e2e.sh), so it's safe on an existing + # volume too — no fresh-volume init script required. + create-pool-indexer-db: + image: postgres:16 + restart: on-failure + depends_on: + - db + environment: + PGHOST: db + PGUSER: $USER + command: + - sh + - -c + - "psql -tc \"SELECT 1 FROM pg_database WHERE datname='pool_indexer'\" | grep -q 1 || psql -c \"CREATE DATABASE pool_indexer\"" + + # Same flyway image as `migrations`, but applies the pool-indexer's own + # migration set to its own database (mirrors the per-network prod DB). + migrations-pool-indexer: + build: + context: . + target: migrations + restart: on-failure + command: migrate + depends_on: + create-pool-indexer-db: + condition: service_completed_successfully + environment: + FLYWAY_URL: jdbc:postgresql://db/pool_indexer?user=$USER&password= + volumes: + - type: bind + source: ./database/sql-pool-indexer/ + target: /flyway/sql + - type: bind + source: ./database/conf/ + target: /flyway/conf + volumes: postgres: