11import * as clc from "colorette" ;
22import { format } from "sql-formatter" ;
33
4- import { IncompatibleSqlSchemaError , Diff , SCHEMA_ID } from "./types" ;
4+ import { IncompatibleSqlSchemaError , Diff , SCHEMA_ID , SchemaValidation } from "./types" ;
55import { getSchema , upsertSchema , deleteConnector } from "./client" ;
66import {
77 setupIAMUsers ,
@@ -23,21 +23,42 @@ import { logLabeledBullet, logLabeledWarning, logLabeledSuccess } from "../utils
2323import * as experiments from "../experiments" ;
2424import * as errors from "./errors" ;
2525
26- export async function diffSchema ( schema : Schema ) : Promise < Diff [ ] > {
26+ export async function diffSchema (
27+ schema : Schema ,
28+ schemaValidation ?: SchemaValidation ,
29+ ) : Promise < Diff [ ] > {
2730 const { serviceName, instanceName, databaseId } = getIdentifiers ( schema ) ;
2831 await ensureServiceIsConnectedToCloudSql (
2932 serviceName ,
3033 instanceName ,
3134 databaseId ,
3235 /* linkIfNotConnected=*/ false ,
3336 ) ;
37+ let diffs : Diff [ ] = [ ] ;
38+
39+ let validationMode : SchemaValidation = "STRICT" ;
40+ if ( experiments . isEnabled ( "fdccompatiblemode" ) ) {
41+ if ( ! schemaValidation ) {
42+ // If the schema validation mode is unset, we surface both STRICT and COMPATIBLE mode diffs, starting with COMPATIBLE.
43+ validationMode = "COMPATIBLE" ;
44+ } else {
45+ validationMode = schemaValidation ;
46+ }
47+ }
48+ setSchemaValidationMode ( schema , validationMode ) ;
3449
35- setCompatibleMode ( schema , databaseId , instanceName ) ;
3650 try {
51+ if ( ! schemaValidation && experiments . isEnabled ( "fdccompatiblemode" ) ) {
52+ logLabeledBullet ( "dataconnect" , `generating required schema changes...` ) ;
53+ }
3754 await upsertSchema ( schema , /** validateOnly=*/ true ) ;
38- logLabeledSuccess ( "dataconnect" , `Database schema is up to date.` ) ;
55+ if ( validationMode === "STRICT" ) {
56+ logLabeledSuccess ( "dataconnect" , `Database schema is up to date.` ) ;
57+ } else {
58+ logLabeledSuccess ( "dataconnect" , `Database schema is compatible.` ) ;
59+ }
3960 } catch ( err : any ) {
40- if ( err . status !== 400 ) {
61+ if ( err ? .status !== 400 ) {
4162 throw err ;
4263 }
4364 const invalidConnectors = errors . getInvalidConnectors ( err ) ;
@@ -52,11 +73,47 @@ export async function diffSchema(schema: Schema): Promise<Diff[]> {
5273 displayInvalidConnectors ( invalidConnectors ) ;
5374 }
5475 if ( incompatible ) {
55- displaySchemaChanges ( incompatible ) ;
56- return incompatible . diffs ;
76+ displaySchemaChanges ( incompatible , validationMode , instanceName , databaseId ) ;
77+ diffs = incompatible . diffs ;
5778 }
5879 }
59- return [ ] ;
80+
81+ if ( experiments . isEnabled ( "fdccompatiblemode" ) ) {
82+ // If the validation mode is unset, then we also surface any additional optional STRICT diffs.
83+ if ( ! schemaValidation ) {
84+ validationMode = "STRICT" ;
85+ setSchemaValidationMode ( schema , validationMode ) ;
86+ try {
87+ logLabeledBullet ( "dataconnect" , `generating schema changes, including optional changes...` ) ;
88+ await upsertSchema ( schema , /** validateOnly=*/ true ) ;
89+ logLabeledSuccess ( "dataconnect" , `no additional optional changes` ) ;
90+ } catch ( err : any ) {
91+ if ( err ?. status !== 400 ) {
92+ throw err ;
93+ }
94+ const incompatible = errors . getIncompatibleSchemaError ( err ) ;
95+ if ( incompatible ) {
96+ if ( ! diffsEqual ( diffs , incompatible . diffs ) ) {
97+ if ( diffs . length === 0 ) {
98+ displaySchemaChanges (
99+ incompatible ,
100+ "STRICT_AFTER_COMPATIBLE" ,
101+ instanceName ,
102+ databaseId ,
103+ ) ;
104+ } else {
105+ displaySchemaChanges ( incompatible , validationMode , instanceName , databaseId ) ;
106+ }
107+ // Return STRICT diffs if the --json flag is passed and schemaValidation is unset.
108+ diffs = incompatible . diffs ;
109+ } else {
110+ logLabeledSuccess ( "dataconnect" , `no additional optional changes` ) ;
111+ }
112+ }
113+ }
114+ }
115+ }
116+ return diffs ;
60117}
61118
62119export async function migrateSchema ( args : {
@@ -75,13 +132,14 @@ export async function migrateSchema(args: {
75132 /* linkIfNotConnected=*/ true ,
76133 ) ;
77134
78- setCompatibleMode ( schema , databaseId , instanceName ) ;
135+ const validationMode = experiments . isEnabled ( "fdccompatiblemode" ) ? "COMPATIBLE" : "STRICT" ;
136+ setSchemaValidationMode ( schema , validationMode ) ;
79137
80138 try {
81139 await upsertSchema ( schema , validateOnly ) ;
82140 logger . debug ( `Database schema was up to date for ${ instanceId } :${ databaseId } ` ) ;
83141 } catch ( err : any ) {
84- if ( err . status !== 400 ) {
142+ if ( err ? .status !== 400 ) {
85143 throw err ;
86144 }
87145 // Parse and handle failed precondition errors, then retry.
@@ -94,9 +152,11 @@ export async function migrateSchema(args: {
94152
95153 const migrationMode = await promptForSchemaMigration (
96154 options ,
155+ instanceName ,
97156 databaseId ,
98157 incompatible ,
99158 validateOnly ,
159+ validationMode ,
100160 ) ;
101161
102162 const shouldDeleteInvalidConnectors = await promptForInvalidConnectorError (
@@ -129,22 +189,26 @@ export async function migrateSchema(args: {
129189 return [ ] ;
130190}
131191
132- function setCompatibleMode ( schema : Schema , databaseId : string , instanceName : string ) {
133- if ( experiments . isEnabled ( "fdccompatiblemode" ) ) {
134- if ( schema . primaryDatasource . postgresql ?. schemaValidation ) {
135- schema . primaryDatasource . postgresql . schemaValidation = "COMPATIBLE" ;
136- } else {
137- schema . primaryDatasource = {
138- postgresql : {
139- database : databaseId ,
140- cloudSql : {
141- instance : instanceName ,
142- } ,
143- schemaValidation : "COMPATIBLE" ,
144- } ,
145- } ;
192+ function diffsEqual ( x : Diff [ ] , y : Diff [ ] ) : boolean {
193+ if ( x . length !== y . length ) {
194+ return false ;
195+ }
196+ for ( let i = 0 ; i < x . length ; i ++ ) {
197+ if (
198+ x [ i ] . description !== y [ i ] . description ||
199+ x [ i ] . destructive !== y [ i ] . destructive ||
200+ x [ i ] . sql !== y [ i ] . sql
201+ ) {
202+ return false ;
146203 }
147204 }
205+ return true ;
206+ }
207+
208+ function setSchemaValidationMode ( schema : Schema , schemaValidation : SchemaValidation ) {
209+ if ( experiments . isEnabled ( "fdccompatiblemode" ) && schema . primaryDatasource . postgresql ) {
210+ schema . primaryDatasource . postgresql . schemaValidation = schemaValidation ;
211+ }
148212}
149213
150214function getIdentifiers ( schema : Schema ) : {
@@ -274,14 +338,16 @@ async function handleIncompatibleSchemaError(args: {
274338
275339async function promptForSchemaMigration (
276340 options : Options ,
277- databaseName : string ,
341+ instanceName : string ,
342+ databaseId : string ,
278343 err : IncompatibleSqlSchemaError | undefined ,
279344 validateOnly : boolean ,
345+ schemaValidation : SchemaValidation ,
280346) : Promise < "none" | "all" > {
281347 if ( ! err ) {
282348 return "none" ;
283349 }
284- displaySchemaChanges ( err ) ;
350+ displaySchemaChanges ( err , schemaValidation , instanceName , databaseId ) ;
285351 if ( ! options . nonInteractive ) {
286352 if ( validateOnly && options . force ) {
287353 // `firebase dataconnect:sql:migrate --force` performs all migrations
@@ -299,7 +365,7 @@ async function promptForSchemaMigration(
299365 { name : "Abort changes" , value : "none" } ,
300366 ] ;
301367 return await promptOnce ( {
302- message : `Would you like to execute these changes against ${ databaseName } ?` ,
368+ message : `Would you like to execute these changes against ${ databaseId } ?` ,
303369 type : "list" ,
304370 choices,
305371 } ) ;
@@ -434,21 +500,51 @@ async function ensureServiceIsConnectedToCloudSql(
434500 try {
435501 await upsertSchema ( currentSchema , /** validateOnly=*/ false ) ;
436502 } catch ( err : any ) {
437- if ( err . status >= 500 ) {
503+ if ( err ? .status >= 500 ) {
438504 throw err ;
439505 }
440506 logger . debug ( err ) ;
441507 }
442508}
443509
444- function displaySchemaChanges ( error : IncompatibleSqlSchemaError ) {
510+ function displaySchemaChanges (
511+ error : IncompatibleSqlSchemaError ,
512+ schemaValidation : SchemaValidation | "STRICT_AFTER_COMPATIBLE" ,
513+ instanceName : string ,
514+ databaseId : string ,
515+ ) {
445516 switch ( error . violationType ) {
446517 case "INCOMPATIBLE_SCHEMA" :
447518 {
448- const message =
449- "Your new schema is incompatible with the schema of your CloudSQL database. " +
450- "The following SQL statements will migrate your database schema to match your new Data Connect schema.\n" +
451- error . diffs . map ( toString ) . join ( "\n" ) ;
519+ let message ;
520+ if ( schemaValidation === "COMPATIBLE" ) {
521+ message =
522+ "Your new application schema is incompatible with the schema of your PostgreSQL database " +
523+ databaseId +
524+ " in your CloudSQL instance " +
525+ instanceName +
526+ ". " +
527+ "The following SQL statements will migrate your database schema to be compatible with your new Data Connect schema.\n" +
528+ error . diffs . map ( toString ) . join ( "\n" ) ;
529+ } else if ( schemaValidation === "STRICT_AFTER_COMPATIBLE" ) {
530+ message =
531+ "Your new application schema is compatible with the schema of your PostgreSQL database " +
532+ databaseId +
533+ " in your CloudSQL instance " +
534+ instanceName +
535+ ", but contains unused tables or columns. " +
536+ "The following optional SQL statements will migrate your database schema to match your new Data Connect schema.\n" +
537+ error . diffs . map ( toString ) . join ( "\n" ) ;
538+ } else {
539+ message =
540+ "Your new application schema does not match the schema of your PostgreSQL database " +
541+ databaseId +
542+ " in your CloudSQL instance " +
543+ instanceName +
544+ ". " +
545+ "The following SQL statements will migrate your database schema to match your new Data Connect schema.\n" +
546+ error . diffs . map ( toString ) . join ( "\n" ) ;
547+ }
452548 logLabeledWarning ( "dataconnect" , message ) ;
453549 }
454550 break ;
0 commit comments