-
Notifications
You must be signed in to change notification settings - Fork 77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixes #25713: ReportsExecution doesn't have timezone on all fields #5959
base: branches/rudder/8.1
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package bootstrap.liftweb.checks.migration | ||
|
||
/* | ||
************************************************************************************* | ||
* Copyright 2024 Normation SAS | ||
************************************************************************************* | ||
* | ||
* This file is part of Rudder. | ||
* | ||
* Rudder is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* In accordance with the terms of section 7 (7. Additional Terms.) of | ||
* the GNU General Public License version 3, the copyright holders add | ||
* the following Additional permissions: | ||
* Notwithstanding to the terms of section 5 (5. Conveying Modified Source | ||
* Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General | ||
* Public License version 3, when you create a Related Module, this | ||
* Related Module is not considered as a part of the work and may be | ||
* distributed under the license agreement of your choice. | ||
* A "Related Module" means a set of sources files including their | ||
* documentation that, without modification of the Source Code, enables | ||
* supplementary functions or services in addition to those offered by | ||
* the Software. | ||
* | ||
* Rudder is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with Rudder. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
* | ||
************************************************************************************* | ||
*/ | ||
|
||
import bootstrap.liftweb.* | ||
import com.normation.errors.IOResult | ||
import com.normation.rudder.db.Doobie | ||
import com.normation.zio.* | ||
import doobie.Update0 | ||
import doobie.implicits.* | ||
import doobie.util.fragment.Fragment | ||
import zio.interop.catz.* | ||
|
||
/* | ||
* When we created table ReportsExecution, we didn't use "timestamp with time zone" for column | ||
* 'insertiondate' and 'compliancecomputationdate' | ||
*/ | ||
class CheckTableReportsExecutionTz( | ||
doobie: Doobie | ||
) extends BootstrapChecks { | ||
|
||
import CheckTableReportsExecutionTz.* | ||
import doobie.* | ||
|
||
override def description: String = | ||
"Check if database table ReportsExecution has timestamps with time zone and correct it if not" | ||
|
||
def addTz(table: String, column: String, overrideChange: Option[Fragment] = None): IOResult[Unit] = { | ||
|
||
transactIOResult(s"Error when adding time zone to column ${table}.${column}")(xa => | ||
query(table, column, overrideChange).run.transact(xa) | ||
).unit | ||
} | ||
|
||
def migrationProg(table: String, overrideChange: Option[Fragment] = None): IOResult[Unit] = { | ||
for { | ||
_ <- addTz(table, insertionDateColumn, overrideChange) | ||
_ <- addTz(table, complianceComputationDateColumn, overrideChange) | ||
} yield () | ||
} | ||
|
||
override def checks(): Unit = { | ||
// Actually run the migration async to avoid blocking for that. | ||
// There is no need to have it sync. | ||
migrationProg(reportsExecution) | ||
.catchAll(err => BootstrapLogger.error(s"Error when trying to add time zone to ReportsExecution table: ${err.fullMsg}")) | ||
.forkDaemon | ||
.runNow | ||
} | ||
|
||
} | ||
|
||
object CheckTableReportsExecutionTz { | ||
val reportsExecution: String = "reportsexecution" | ||
val insertionDateColumn: String = "insertiondate" | ||
val complianceComputationDateColumn: String = "compliancecomputationdate" | ||
|
||
// overrideChange is only used for tests | ||
def query(table: String, column: String, overrideChange: Option[Fragment] = None): Update0 = { | ||
def toQuotedFr(s: String) = Fragment.const("'" + s + "'") | ||
def toFr(s: String) = Fragment.const(s) | ||
|
||
val alter: Fragment = fr"ALTER TABLE " ++ toFr(table) ++ fr" ALTER COLUMN " ++ toFr( | ||
column | ||
) ++ fr" TYPE TIMESTAMP WITH TIME ZONE USING " ++ toFr(column) ++ fr" AT TIME ZONE 'UTC';" | ||
|
||
val sql = { | ||
fr""" | ||
DO $$$$ BEGIN | ||
IF NOT EXISTS ( | ||
select column_name, data_type | ||
from information_schema.columns | ||
where table_name = """ ++ toQuotedFr(table) ++ fr" and column_name = " ++ toQuotedFr( | ||
column | ||
) ++ fr""" and data_type = 'timestamp with time zone' | ||
Comment on lines
+108
to
+110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The table and column does not need to be constant fragment but can simply be interpolated, since they are standard parameterized arguments for the query, and not part of the SQL DML syntax, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added and fixed the quoting convention - we always use |
||
) | ||
THEN | ||
""" ++ overrideChange.getOrElse(alter) ++ fr""" | ||
end if; | ||
END $$$$;""" | ||
} | ||
|
||
sql.update | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
/* | ||
************************************************************************************* | ||
* Copyright 2024 Normation SAS | ||
************************************************************************************* | ||
* | ||
* This file is part of Rudder. | ||
* | ||
* Rudder is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* In accordance with the terms of section 7 (7. Additional Terms.) of | ||
* the GNU General Public License version 3, the copyright holders add | ||
* the following Additional permissions: | ||
* Notwithstanding to the terms of section 5 (5. Conveying Modified Source | ||
* Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General | ||
* Public License version 3, when you create a Related Module, this | ||
* Related Module is not considered as a part of the work and may be | ||
* distributed under the license agreement of your choice. | ||
* A "Related Module" means a set of sources files including their | ||
* documentation that, without modification of the Source Code, enables | ||
* supplementary functions or services in addition to those offered by | ||
* the Software. | ||
* | ||
* Rudder is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with Rudder. If not, see <http://www.gnu.org/licenses/>. | ||
* | ||
************************************************************************************* | ||
*/ | ||
|
||
package bootstrap.liftweb.checks.migration | ||
|
||
import com.normation.rudder.db.DBCommon | ||
import com.normation.zio.UnsafeRun | ||
import doobie.* | ||
import doobie.specs2.IOChecker | ||
import doobie.syntax.all.* | ||
import doobie.util.fragments.whereAnd | ||
import doobie.util.transactor.Transactor | ||
import org.junit.runner.RunWith | ||
import org.specs2.runner.JUnitRunner | ||
import org.specs2.specification.core.Fragments | ||
import zio.* | ||
import zio.interop.catz.* | ||
|
||
@RunWith(classOf[JUnitRunner]) | ||
class TestMigrateTableReportsExecution extends DBCommon with IOChecker { | ||
import CheckTableReportsExecutionTz.* | ||
|
||
val testTable = "reportsexecutiontest" | ||
|
||
private lazy val checkTableReportsExecutionTz = new CheckTableReportsExecutionTz(doobie) | ||
|
||
override def transactor: Transactor[cats.effect.IO] = doobie.xaio | ||
|
||
override def initDb(): Unit = { | ||
super.initDb() | ||
doobie.transactRunEither(previousSchemaDDL.update.run.transact(_)) match { | ||
case Right(_) => () | ||
case Left(ex) => throw ex | ||
} | ||
} | ||
|
||
// The previous schema, with the renamed table for this test | ||
// We need to know for sure the initial state and the final state of the migrated table, | ||
// so we define and use the previous schema without conflicting with the current one (with all values renamed) | ||
private val previousSchemaDDL = sql""" | ||
CREATE TABLE reportsexecutiontest ( | ||
nodeId text NOT NULL | ||
, date timestamp with time zone NOT NULL | ||
, nodeConfigId text | ||
, insertionId bigint | ||
, insertiondate timestamp default now() | ||
, compliancecomputationdate timestamp | ||
, PRIMARY KEY(nodeId, date) | ||
); | ||
|
||
CREATE INDEX reportsexecution_test_date_idx ON reportsexecutiontest (date); | ||
CREATE INDEX reportsexecution_test_nodeid_nodeconfigid_idx ON reportsexecutiontest (nodeId, nodeConfigId); | ||
CREATE INDEX reportsexecution_test_uncomputedrun_idx on reportsexecutiontest (compliancecomputationdate) where compliancecomputationdate IS NULL; | ||
""" | ||
|
||
sequential | ||
|
||
"MigrateEventLogEnforceSchema" should { | ||
|
||
"check migration queries" in { | ||
// check that the alter statements are made on existing table and column | ||
if (doDatabaseConnection) { | ||
check(query(testTable, insertionDateColumn)) | ||
check(query(testTable, complianceComputationDateColumn)) | ||
} else Fragments.empty | ||
} | ||
|
||
"migrate all columns successfully" in { | ||
|
||
checkTableReportsExecutionTz.migrationProg(testTable).runNow // await for the migration before running assertions test | ||
|
||
val sql = { | ||
fr"""SELECT DISTINCT column_name, data_type FROM INFORMATION_SCHEMA.COLUMNS | ||
""" ++ whereAnd( | ||
fr"table_name = " ++ Fragment.const("'" + testTable + "'"), | ||
fr"column_name in (" ++ Fragment.const("'" + insertionDateColumn + "'") | ||
) ++ fr", " ++ Fragment.const("'" + complianceComputationDateColumn + "'") ++ fr")" | ||
Comment on lines
+109
to
+111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since it's a parameterized queries, it can just be interpolated within the fragment, no ? fr"table_name = ${testTable}" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. well, yes, but not in the ALTER TABLE part, so I wanted to make it very clear that we can't use the same things in both place by using the same construct with a different escaping rule. |
||
} | ||
|
||
// check that all migrations are eventually applied even in async mode | ||
doobie.transactRunEither( | ||
sql | ||
.query[(String, String)] | ||
.to[List] | ||
.transact(_) | ||
) match { | ||
case Right(res) => | ||
res must containTheSameElementsAs( // is_nullable is 'NO' for every column | ||
List(insertionDateColumn, complianceComputationDateColumn).map((_, "timestamp with time zone")) | ||
).eventually(10, 100.millis.asScala) | ||
case Left(ex) => | ||
ko( | ||
s"Migration of 'ReportsExecution' table lead to error : ${ex.getMessage}" | ||
) | ||
} | ||
} | ||
|
||
"a second migration doesn't do the 'if' part" in { | ||
val res = { | ||
checkTableReportsExecutionTz | ||
.migrationProg(testTable, Some(fr"RAISE EXCEPTION 'oh no!'; ")) | ||
.either | ||
.runNow // await for the migration before running assertions test | ||
} | ||
res must beRight | ||
} | ||
|
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure for the 'UTC' timezone, in our Doobie Meta we write dates using no specific timezone and I thought the PG session current timezone is used (
SHOW TIMEZONE;
), so maybe 'UTC' is a special test value here ?I could not test the behavior because I have no reports locally but I believe that if the timezone is something else than UTC we could end up assigning wrong values to the columns
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't any information about what was used to write these timezone, this is lost for ever. The server config can have changed. And since it's not really use, it doesn't not matter. UTC is a as correct value, as other and in case of problem it will be much more easy to guess what happened if we don't have to check for what was the timezone when the migration was done.