runningInConsole()&&app()->environment('budget-testing'),403); return CompositionRuntime::physical(); } /** Callback may prove installed structures inside the same transaction. * Default is a full rollback. No role, grant, account, association or document is created. */ public function run(bool $commit=false,?\Closure $verify=null):array { $identity=self::identity();$host=DB::connection(); abort_unless($host->transactionLevel()===0,409,'Não compor dentro de uma transacção alheia.'); $constraint=$host->selectOne("SELECT pg_get_constraintdef(oid) value FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->value??null; if(!is_string($constraint)||!preg_match('/^CHECK \((.*)\)$/s',$constraint,$m))throw new \LogicException('COMPOSITION_PERMISSION_CHECK_MISSING'); $connection=new CompositionSchemaConnection($host->getPdo(),$host->getDatabaseName(),'',$host->getConfig()); $connection->originalPermissionCheck=$m[1]; $name='sgf_composition_schema';$original=DB::getDefaultConnection(); config(['database.connections.'.$name=>$host->getConfig()]);DB::extend($name,fn()=>$connection);DB::setDefaultConnection($name);Schema::clearResolvedInstance('db.schema');app()->forgetInstance('db.schema'); $result=[];$connection->beginTransaction(); try{ $connection->statement("SET LOCAL lock_timeout='5s'");$connection->statement("SET LOCAL statement_timeout='30s'"); $connection->selectOne("SELECT pg_advisory_xact_lock(hashtext('sgf-single-installation-schema'))"); if($connection->getSchemaBuilder()->hasTable('sgf_composition_schema')){ $stored=$connection->table('sgf_composition_schema')->where('id',1)->first(); if(!$stored||$stored->source_hash!==$this->fingerprint())throw new \LogicException('COMPOSITION_SCHEMA_VERSION_CHANGED'); $result=['identity'=>$identity,'replayed'=>true,'source_hash'=>$stored->source_hash,'reused_tables'=>[],'migrations'=>[]]; }else{ $before=array_column($connection->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"),'tablename'); foreach(['b_internal_confirmations','doc_records','wo_orders','sr_records','ca_proposals'] as $table)if(in_array($table,$before,true))throw new \LogicException('COMPOSITION_PARTIAL_SCHEMA_REQUIRES_REVIEW: '.$table); if(!$connection->selectOne("SELECT to_regprocedure('public.sgf_append_only_audit()') value")->value)$connection->unprepared("CREATE FUNCTION sgf_append_only_audit() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN RAISE EXCEPTION 'SGF_APPEND_ONLY_AUDIT'; END; \$\$;"); $migrations=[]; $auditTables=['a_history','a_events','a_commands','a_outbox','a_pending'];$present=array_intersect($auditTables,$before); if(count($present)!==0&&count($present)!==count($auditTables))throw new \LogicException('COMPOSITION_PARTIAL_A_HISTORY'); foreach(self::MIGRATIONS as $index=>$file){ if($index===0&&count($present)===count($auditTables)){ foreach(['a_clients','a_services','a_service_drafts','a_billing_drafts'] as $table){ if(!in_array($table,$before,true))throw new \LogicException('COMPOSITION_PARTIAL_A_RECORDS'); $connection->getSchemaBuilder()->create($table,fn()=>null); } $guards=$connection->selectOne("SELECT count(*) n FROM pg_trigger WHERE NOT tgisinternal AND tgenabled='O' AND tgname IN ('a_clients_guard','a_services_guard','a_service_drafts_guard','a_billing_drafts_guard','a_history_scope','a_history_append_only','a_events_append_only','a_commands_append_only','a_outbox_append_only','a_pending_append_only')")->n; if((int)$guards!==10)throw new \LogicException('COMPOSITION_A_GUARDS_INCOMPLETE'); continue; } $migration=require database_path('migrations/'.$file);$migration->up();$migrations[]=$file; } $connection->unprepared('CREATE TABLE sgf_composition_schema(id smallint PRIMARY KEY CHECK(id=1),source_hash char(64) NOT NULL,installed_at timestamptz NOT NULL DEFAULT now())'); $connection->table('sgf_composition_schema')->insert(['id'=>1,'source_hash'=>$this->fingerprint()]); $connection->unprepared('CREATE TRIGGER composition_schema_append_only BEFORE UPDATE OR DELETE ON sgf_composition_schema FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit()'); // Rebind ONLY the newly created QA guards to this exact database, // role and cluster. The source SQL and all other installations retain // their original guard; no general local/development bypass is added. foreach(['wo','sr','dc','ic'] as $prefix){ $connection->unprepared("CREATE OR REPLACE FUNCTION sgf_{$prefix}_environment() RETURNS void LANGUAGE plpgsql AS \$\$ BEGIN IF current_database() IS DISTINCT FROM '".$identity['database']."' OR current_user IS DISTINCT FROM '".$identity['role']."' OR inet_server_port() IS DISTINCT FROM 55439 OR current_setting('cluster_name') IS DISTINCT FROM '".$identity['cluster']."' THEN RAISE EXCEPTION 'SGF_COMPOSITION_OWNED_QA_ONLY'; END IF; END; \$\$;"); } $after=array_column($connection->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"),'tablename'); $result=['identity'=>$identity,'replayed'=>false,'source_hash'=>$this->fingerprint(),'reused_tables'=>$connection->getSchemaBuilder()->reused,'new_tables'=>array_values(array_diff($after,$before)),'migrations'=>$migrations]; } if($verify)$result['verification']=$verify($connection); if($commit)$connection->commit();else $connection->rollBack(); $result['committed']=$commit;return $result; }finally{ while($connection->transactionLevel()>0)$connection->rollBack(); DB::setDefaultConnection($original);Schema::clearResolvedInstance('db.schema');app()->forgetInstance('db.schema');DB::purge($name); } } public function fingerprint():string { $files=[];foreach(self::MIGRATIONS as $file)$files['database/migrations/'.$file]=hash_file('sha256',database_path('migrations/'.$file)); foreach(glob(database_path('sql/*.sql')) as $file)$files['database/sql/'.basename($file)]=hash_file('sha256',$file); foreach(['CompositionSchema.php','CompositionSchemaConnection.php','CompositionSchemaBuilder.php','CompositionRuntime.php'] as $file)$files[$file]=hash_file('sha256',__DIR__.'/'.$file); return hash('sha256',json_encode($files,JSON_THROW_ON_ERROR)); } }