@@ -12,8 +12,9 @@ import { tableBillingMock, tableBillingMockFns } from '@sim/testing/mocks/table-
1212import { tableTriggerMock } from '@sim/testing/mocks/table-trigger.mock'
1313import { tableWorkflowColumnsMock } from '@sim/testing/mocks/table-workflow-columns.mock'
1414import { sleep } from '@sim/utils/helpers'
15- import { generateId } from '@sim/utils/id'
15+ import { generateId , generateShortId } from '@sim/utils/id'
1616import { sql } from 'drizzle-orm'
17+ import { drizzle } from 'drizzle-orm/postgres-js'
1718import postgres from 'postgres'
1819import { afterAll , beforeAll , beforeEach , describe , expect , it , vi } from 'vitest'
1920
@@ -23,7 +24,6 @@ vi.mock('@/lib/table/workflow-columns', () => tableWorkflowColumnsMock)
2324vi . mock ( '@/lib/uploads/core/storage-service' , ( ) => storageServiceMock )
2425
2526import { USER_TABLE_ROWS_SQL_NAME } from '@/lib/table/constants'
26- import { withSeqscanOff } from '@/lib/table/planner'
2727import {
2828 batchInsertRows ,
2929 deleteRowsByFilter ,
@@ -45,15 +45,10 @@ const control = postgres(url, { max: 4, onnotice: () => {} })
4545const workspaceId = generateId ( )
4646const userId = generateId ( )
4747
48- /**
49- * `db:push` never installs migration-only objects: the `rows_version` triggers and the
50- * `(workspace_id, table_id)` dependency statistic. Only the cases that depend on them need them.
51- */
52- const [ { migrated } ] = await control < { migrated : boolean } [ ] > `SELECT
53- EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'user_table_rows_version_update_trigger')
54- AND EXISTS (
55- SELECT 1 FROM pg_statistic_ext WHERE stxname = 'user_table_rows_workspace_table_stats'
56- ) AS migrated`
48+ /** `db:push` never installs the migration-only `rows_version` triggers; only those cases need them. */
49+ const [ { migrated } ] = await control < { migrated : boolean } [ ] > `SELECT EXISTS (
50+ SELECT 1 FROM pg_trigger WHERE tgname = 'user_table_rows_version_update_trigger'
51+ ) AS migrated`
5752
5853async function createTable ( columns : ColumnDefinition [ ] ) : Promise < TableDefinition > {
5954 const id = generateId ( )
@@ -80,6 +75,7 @@ async function rowsVersion(tableId: string): Promise<number> {
8075interface QueryPlan {
8176 'Node Type' : string
8277 'Index Name' ?: string
78+ 'Subplan Name' ?: string
8379 Plans ?: QueryPlan [ ]
8480}
8581
@@ -383,36 +379,74 @@ describe('table row writes against real PostgreSQL', () => {
383379
384380 expect ( [ ...result . affectedRowIds ] . sort ( ) ) . toEqual ( [ ...expected ] . sort ( ) )
385381 } )
382+ } )
386383
387- /** A table large enough that the planner's index choice reflects its statistics. */
388- async function seedPlanFixture ( ) : Promise < ( filter : Filter ) => Promise < QueryPlan [ ] > > {
389- const table = await createTable ( textColumns ( 'kind' , 'n' ) )
390- await control `INSERT INTO user_table_rows (id, table_id, workspace_id, data, order_key)
391- SELECT ${ table . id } || '-' || n, ${ table . id } , ${ workspaceId } ,
384+ /**
385+ * Plans come from a private copy of `user_table_rows` (same columns and indexes, no triggers) in
386+ * its own schema, holding only this fixture and analyzed from a full sample. Integration files
387+ * share one database in parallel, so plans against the shared relation would depend on every
388+ * other suite's rows and statistics.
389+ */
390+ describe ( 'limited filtered query plans' , ( ) => {
391+ const planSchema = `row_writes_${ generateShortId ( )
392+ . replace ( / [ ^ a - z A - Z 0 - 9 ] / g, '' )
393+ . toLowerCase ( ) } `
394+ const planConnection = postgres ( url , {
395+ max : 1 ,
396+ onnotice : ( ) => { } ,
397+ connection : { search_path : `${ planSchema } , public` } ,
398+ } )
399+ const planTable : TableDefinition = {
400+ id : 'table-plan' ,
401+ workspaceId : 'workspace-plan' ,
402+ name : 'Plan fixture' ,
403+ description : null ,
404+ schema : { columns : textColumns ( 'kind' , 'n' ) } ,
405+ metadata : null ,
406+ rowCount : 20_000 ,
407+ maxRows : 100_000 ,
408+ createdBy : 'user-plan' ,
409+ locks : { schemaLocked : false , insertLocked : false , updateLocked : false , deleteLocked : false } ,
410+ archivedAt : null ,
411+ createdAt : new Date ( 0 ) ,
412+ updatedAt : new Date ( 0 ) ,
413+ }
414+
415+ beforeAll ( async ( ) => {
416+ await planConnection `CREATE SCHEMA ${ planConnection ( planSchema ) } `
417+ await planConnection `CREATE TABLE user_table_rows
418+ (LIKE public.user_table_rows INCLUDING DEFAULTS INCLUDING INDEXES)`
419+ await planConnection `INSERT INTO user_table_rows (id, table_id, workspace_id, data, order_key)
420+ SELECT 'row-' || n, ${ planTable . id } , ${ planTable . workspaceId } ,
392421 jsonb_build_object('kind', CASE WHEN n % 500 = 0 THEN 'hit' ELSE 'miss' END, 'n', n::text),
393422 'a' || lpad(n::text, 6, '0')
394423 FROM generate_series(1, 20000) AS n`
395- await control `ANALYZE user_table_rows`
424+ await planConnection `ANALYZE user_table_rows`
425+ } )
396426
397- return async ( filter ) => {
398- const filterClause = buildFilterClause (
399- filter ,
400- USER_TABLE_ROWS_SQL_NAME ,
401- table . schema . columns
402- )
403- if ( ! filterClause ) throw new Error ( 'Fixture filter compiled to no clause' )
404- const plans = await withSeqscanOff ( ( trx ) =>
405- trx . execute < { 'QUERY PLAN' : { Plan : QueryPlan } [ ] } > (
406- sql `EXPLAIN (FORMAT JSON) ${ firstMatchingRowsQuery ( table , filter , filterClause , 5 , 'rows' ) } `
407- )
427+ afterAll ( async ( ) => {
428+ await planConnection `DROP SCHEMA ${ planConnection ( planSchema ) } CASCADE`
429+ await planConnection . end ( )
430+ } )
431+
432+ async function planFor ( filter : Filter ) : Promise < QueryPlan [ ] > {
433+ const filterClause = buildFilterClause (
434+ filter ,
435+ USER_TABLE_ROWS_SQL_NAME ,
436+ planTable . schema . columns
437+ )
438+ if ( ! filterClause ) throw new Error ( 'Fixture filter compiled to no clause' )
439+ const query = firstMatchingRowsQuery ( planTable , filter , filterClause , 5 , 'rows' )
440+ const plans = await drizzle ( planConnection ) . transaction ( async ( trx ) => {
441+ await trx . execute ( sql `SET LOCAL enable_seqscan = off` )
442+ return trx . execute < { 'QUERY PLAN' : { Plan : QueryPlan } [ ] } > (
443+ sql `EXPLAIN (FORMAT JSON) ${ query } `
408444 )
409- return planNodes ( plans [ 0 ] [ 'QUERY PLAN' ] [ 0 ] . Plan )
410- }
445+ } )
446+ return planNodes ( plans [ 0 ] [ 'QUERY PLAN' ] [ 0 ] . Plan )
411447 }
412448
413449 it ( 'fences only a containment filter and leaves any other filter unfenced' , async ( ) => {
414- const planFor = await seedPlanFixture ( )
415-
416450 const fenced = await planFor ( { kind : 'hit' } )
417451 expect ( fenced . some ( ( node ) => node [ 'Node Type' ] === 'CTE Scan' ) ) . toBe ( true )
418452
@@ -421,17 +455,16 @@ describe('table row writes against real PostgreSQL', () => {
421455 expect ( walked [ 0 ] [ 'Node Type' ] ) . toBe ( 'Limit' )
422456 } )
423457
424- it . skipIf ( ! migrated ) ( 'probes the tenant GIN index inside the fence' , async ( ) => {
425- const planFor = await seedPlanFixture ( )
426-
427- const fenced = await planFor ( { kind : 'hit' } )
428- expect (
429- fenced . some (
430- ( node ) =>
431- node [ 'Node Type' ] === 'Bitmap Index Scan' &&
432- node [ 'Index Name' ] === 'user_table_rows_tenant_data_gin_idx'
433- )
434- ) . toBe ( true )
458+ it ( 'probes the containment GIN index inside the fence' , async ( ) => {
459+ const matched = ( await planFor ( { kind : 'hit' } ) ) . find (
460+ ( node ) => node [ 'Subplan Name' ] === 'CTE matched'
461+ )
462+ const probe =
463+ matched && planNodes ( matched ) . find ( ( node ) => node [ 'Node Type' ] === 'Bitmap Index Scan' )
464+ const [ index ] = await planConnection < { indexdef : string } [ ] > `SELECT indexdef FROM pg_indexes
465+ WHERE schemaname = ${ planSchema } AND indexname = ${ probe ?. [ 'Index Name' ] ?? '' } `
466+ expect ( index ?. indexdef ) . toContain ( 'USING gin' )
467+ expect ( index ?. indexdef ) . toContain ( 'jsonb_path_ops' )
435468 } )
436469 } )
437470
@@ -442,7 +475,8 @@ describe('table row writes against real PostgreSQL', () => {
442475
443476 /**
444477 * Holds the table's row-order lock while both inserts start, so each one's pre-insert work runs
445- * before either can write. Released once both wait on the lock.
478+ * before either can write. Released only once both are seen waiting on this table's lock;
479+ * otherwise the inserts could run one after the other and prove nothing about the race.
446480 */
447481 async function raceUnderHeldOrderLock (
448482 tableId : string ,
@@ -453,13 +487,18 @@ describe('table row writes against real PostgreSQL', () => {
453487 await holder `BEGIN`
454488 await holder `SELECT pg_advisory_xact_lock(hashtextextended(${ `user_table_rows_pos:${ tableId } ` } , 0))`
455489 const racers = Promise . allSettled ( [ insert ( ) , insert ( ) ] )
456- for ( let attempt = 0 ; attempt < 400 ; attempt ++ ) {
457- const [ { waiting } ] = await control < { waiting : number } [ ] > `SELECT count(*)::int AS waiting
458- FROM pg_locks WHERE locktype = 'advisory' AND NOT granted
459- AND database = (SELECT oid FROM pg_database WHERE datname = current_database())`
460- if ( waiting >= 2 ) break
490+ let waiting = 0
491+ for ( let attempt = 0 ; attempt < 400 && waiting < 2 ; attempt ++ ) {
461492 await sleep ( 5 )
493+ ; [ { waiting } ] = await control < { waiting : number } [ ] > `
494+ WITH lock AS (SELECT hashtextextended(${ `user_table_rows_pos:${ tableId } ` } , 0) AS key)
495+ SELECT count(*)::int AS waiting FROM pg_locks, lock
496+ WHERE locktype = 'advisory' AND NOT granted AND objsubid = 1
497+ AND database = (SELECT oid FROM pg_database WHERE datname = current_database())
498+ AND classid = ((lock.key >> 32) & 4294967295)::oid
499+ AND objid = (lock.key & 4294967295)::oid`
462500 }
501+ expect ( waiting ) . toBe ( 2 )
463502 await holder `COMMIT`
464503 return await racers
465504 } finally {
0 commit comments