1+ //> using scala 3
2+
3+ //> using dep org.scastie.old::api::0.30.0-SNAPSHOT
4+ //> using dep org.scastie.old:storage_2.13:0.30.0-SNAPSHOT
5+
6+ //> using dep org.scastie:api_2.13:1.0.0-SNAPSHOT
7+ //> using dep org.scastie:storage_2.13:1.0.0-SNAPSHOT
8+
9+ //> using dep com.typesafe:config:latest.release
10+ //> using dep com.lihaoyi::pprint:latest.release
11+
12+
13+
14+ import org .scastie .storage .postgres .PostgresContainer
15+ import org .scastie .storage .postgres .Snippet
16+
17+ import org .slf4j .LoggerFactory
18+ import ch .qos .logback .classic .{Level , Logger }
19+
20+ import com .olegych .scastie .api .AttachedDom
21+ import com .olegych .scastie .api .Html
22+ import com .olegych .scastie .api .ScalaTarget .Js
23+ import com .olegych .scastie .api .ScalaTarget .Jvm
24+ import com .olegych .scastie .api .ScalaTarget .Native
25+ import com .olegych .scastie .api .ScalaTarget .Scala3
26+ import com .olegych .scastie .api .ScalaTarget .Typelevel
27+ import com .olegych .scastie .api .Value
28+ import com .olegych .scastie .{api => oldApi }
29+ import com .olegych .scastie .{storage => oldStorage }
30+ import com .typesafe .config .ConfigFactory
31+ import io .circe .Encoder
32+ import io .circe .*
33+ import org .mongodb .scala ._
34+ import org .mongodb .scala .model .Projections ._
35+ import org .mongodb .scala .model ._
36+ import org .scastie .runtime .{api => runtimeApi }
37+ import org .scastie .storage .MongoSnippet .mongoSnippetEncoder
38+ import org .scastie .{api => newApi }
39+ import org .scastie .{storage => newStorage }
40+ import play .api .libs .json .Json
41+ import play .api .libs .json .Reads
42+
43+ import scala .concurrent .Await
44+ import scala .concurrent .ExecutionContext .Implicits .global
45+ import scala .concurrent .Future
46+ import scala .concurrent .duration .*
47+ import scala .util .Try
48+
49+ @ main def migrate = {
50+ val rootLogger = LoggerFactory .getLogger(org.slf4j.Logger .ROOT_LOGGER_NAME ).asInstanceOf [Logger ]
51+ rootLogger.setLevel(Level .INFO )
52+ LoggerFactory .getLogger(" com.zaxxer.hikari" ).asInstanceOf [Logger ].setLevel(Level .WARN )
53+ LoggerFactory .getLogger(" org.postgresql" ).asInstanceOf [Logger ].setLevel(Level .WARN )
54+ LoggerFactory .getLogger(" scalasql" ).asInstanceOf [Logger ].setLevel(Level .WARN )
55+ LoggerFactory .getLogger(" org.scastie.storage" ).asInstanceOf [Logger ].setLevel(Level .WARN )
56+ System .setProperty(" org.mongodb.driver.level" , " WARN" )
57+ val mongoUri = {
58+ val config = ConfigFactory .load().getConfig(" scastie.mongodb" )
59+ val user = config.getString(" user" )
60+ val password = config.getString(" password" )
61+ val databaseName = config.getString(" database" )
62+ val host = config.getString(" host" )
63+ val port = config.getInt(" port" )
64+ s " mongodb:// $user: $password@ $host: $port/ $databaseName"
65+ // "mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+1.6.1"
66+ }
67+ val mongoClient : MongoClient =
68+ MongoClient (mongoUri)
69+
70+ val oldDatabase : MongoDatabase = mongoClient.getDatabase(" snippets" )
71+ val oldSnippets = oldDatabase.getCollection[Document ](" snippets" )
72+
73+ val pgContainer : PostgresContainer =
74+ new PostgresContainer (defaultConfig = true )
75+
76+ def fromBson [T ](obj : Document )(implicit reads : Reads [T ]): Option [T ] =
77+ pprint(s " Deserializing BSON: ${obj.toJson().take(10 )}" )
78+ Json .parse(obj.toJson()).asOpt[T ]
79+
80+ def convertType (o : oldApi.ProcessOutputType ): newApi.ProcessOutputType =
81+ o match
82+ case oldApi.ProcessOutputType .StdErr => newApi.ProcessOutputType .StdErr
83+ case oldApi.ProcessOutputType .StdOut => newApi.ProcessOutputType .StdOut
84+
85+ def convertSeverity (o : oldApi.Severity ): newApi.Severity =
86+ o match
87+ case oldApi.Info => newApi.Info
88+ case oldApi.Warning => newApi.Warning
89+ case oldApi.Error => newApi.Error
90+
91+ def convertInstrumentation (instrumentation : oldApi.Instrumentation ): runtimeApi.Instrumentation =
92+ val newPosition = runtimeApi.Position (instrumentation.position.start, instrumentation.position.end)
93+ val newRender = instrumentation.render match
94+ case Value (v, className) => runtimeApi.Value (v, className)
95+ case Html (a, folded) => runtimeApi.Html (a, folded)
96+ case AttachedDom (uuid, folded) => runtimeApi.AttachedDom (uuid, folded)
97+
98+ runtimeApi.Instrumentation (newPosition, newRender)
99+
100+ def convertTarget (target : oldApi.ScalaTarget ): newApi.SbtScalaTarget =
101+ target match
102+ case Jvm (scalaVersion) => newApi.Scala2 (scalaVersion)
103+ case Typelevel (scalaVersion) => newApi.Typelevel (scalaVersion)
104+ case Js (scalaVersion, scalaJsVersion) => newApi.Js (scalaVersion, scalaJsVersion)
105+ case Native (scalaVersion, scalaNativeVersion) => newApi.Native (scalaVersion, scalaNativeVersion)
106+ case Scala3 (scalaVersion) => newApi.Scala3 (scalaVersion)
107+
108+ def convertLibrariesFromList (libs : List [(oldApi.ScalaDependency , oldApi.Project )], newTarget : newApi.SbtScalaTarget ): List [(newApi.ScalaDependency , newApi.Project )] =
109+ libs.map { (l, pro) =>
110+ val newL = newApi.ScalaDependency (l.groupId, l.artifact, newTarget, l.version)
111+ val newPro = newApi.Project (pro.organization, pro.repository, pro.logo, pro.artifacts)
112+
113+ newL -> newPro
114+ }
115+
116+ def convertSnippetId (old : oldApi.SnippetId ): newApi.SnippetId =
117+ val newUser : Option [newApi.SnippetUserPart ] = old.user.map(user => newApi.SnippetUserPart (user.login, user.update))
118+ newApi.SnippetId (base64UUID = old.base64UUID, user = newUser)
119+
120+ def convertSnippet (old : oldStorage.MongoSnippet ) =
121+ import org .scastie .storage .*
122+ val newCode = if old.inputs.code.contains(" import com.olegych" ) && old.oldId != 0 then
123+ old.inputs.code.replace(" import com.olegych" , " import org.scastie" )
124+ else
125+ old.inputs.code
126+
127+ val newProgresses = old.progresses.map(p =>
128+ newApi.SnippetProgress (
129+ ts = p.ts,
130+ id = p.id,
131+ snippetId = p.snippetId.map(convertSnippetId),
132+ userOutput = p.userOutput.map(o => newApi.ProcessOutput (o.line, tpe = convertType(o.tpe), id = o.id)),
133+ buildOutput = p.sbtOutput.map(o => newApi.ProcessOutput (o.line, tpe = convertType(o.tpe), id = o.id)),
134+ compilationInfos = p.compilationInfos.map(problem => newApi.Problem (convertSeverity(problem.severity), problem.line, problem.message)),
135+ instrumentations = p.instrumentations.map(convertInstrumentation),
136+ runtimeError = p.runtimeError.map(er => runtimeApi.RuntimeError (er.message, er.line, er.fullStack)),
137+ scalaJsContent = p.scalaJsContent,
138+ scalaJsSourceMapContent = p.scalaJsSourceMapContent,
139+ isDone = p.isDone,
140+ isTimeout = p.isTimeout,
141+ isSbtError = p.isSbtError,
142+ isForcedProgramMode = p.isForcedProgramMode
143+ )
144+ )
145+
146+ val newTarget = convertTarget(old.inputs.target)
147+ val newInputs : newApi.BaseInputs = newApi.SbtInputs (
148+ isWorksheetMode = old.inputs.isWorksheetMode,
149+ code = newCode,
150+ target = newTarget,
151+ libraries = old.inputs.libraries.map(l => newApi.ScalaDependency (l.groupId, l.artifact, newTarget, l.version)),
152+ librariesFromList = convertLibrariesFromList(old.inputs.librariesFromList, newTarget),
153+ sbtConfigExtra = old.inputs.sbtConfigExtra,
154+ sbtConfigSaved = old.inputs.sbtConfigSaved,
155+ sbtPluginsConfigExtra = old.inputs.sbtPluginsConfigExtra,
156+ sbtPluginsConfigSaved = old.inputs.sbtPluginsConfigSaved,
157+ isShowingInUserProfile = old.inputs.isShowingInUserProfile,
158+ forked = old.inputs.forked.map(convertSnippetId)
159+ )
160+
161+ Snippet (
162+ simpleSnippetId = old.simpleSnippetId,
163+ username = old.user,
164+ snippetId = convertSnippetId(old.snippetId),
165+ inputs = newInputs,
166+ progresses = newProgresses,
167+ scalaJsContent = old.scalaJsContent,
168+ scalaJsSourceMapContent = old.scalaJsSourceMapContent,
169+ time = old.time
170+ )
171+ def saveSnippetInNewDatabase (snippets : Seq [Snippet ]): Future [Unit ] = {
172+ // Przetwarzaj 10 snippetów równocześnie zamiast wszystkich sekwencyjnie
173+ Future .traverse(snippets.grouped(10 ).toSeq) { batch =>
174+ Future .traverse(batch) { s =>
175+ for {
176+ _ <- pgContainer.insertWithExistingId(s.snippetId, s.inputs)
177+ _ <- Future .sequence(s.progresses.map(pgContainer.appendOutput)) // równolegle zamiast .traverse
178+ } yield ()
179+ }
180+ }.map(_ => ())
181+ }
182+
183+ def migrateSnippets (originalSize : Int ): Future [Seq [String ]] = {
184+ pprint.pprintln(s " Starting migration of $originalSize snippets " )
185+
186+ var count = 0L
187+ var progress = 0
188+ val allUrls = scala.collection.mutable.ArrayBuffer .empty[String ]
189+
190+ def step (): Future [Unit ] = {
191+ if (count >= originalSize) {
192+ Future .unit
193+ } else {
194+ oldSnippets
195+ .find()
196+ .skip(count.toInt)
197+ .limit(1000 )
198+ .toFuture()
199+ .flatMap { docs =>
200+ docs.foreach { doc =>
201+ val sOpt = fromBson[oldStorage.MongoSnippet ](doc)
202+ sOpt match {
203+ case Some (s) =>
204+ if (s.user.isEmpty) println(s " Skipping anonymous snippet ${s.snippetId.url}" )
205+ else pprint.pprintln(s " Migrating snippet ${s.snippetId.url}" )
206+ case None =>
207+ pprint.pprintln(s " Could not deserialize snippet: ${doc.toJson()}" )
208+ }
209+ }
210+
211+ val snippets = docs
212+ .flatMap(fromBson[oldStorage.MongoSnippet ])
213+ .flatMap { s =>
214+ if (s.user.isEmpty) None
215+ else Some (convertSnippet(s))
216+ }
217+ count += docs.size
218+
219+ allUrls ++= snippets.map(_.snippetId.url)
220+ saveSnippetInNewDatabase(snippets).flatMap(_ => step())
221+ }
222+ }
223+ }
224+
225+ step().map(_ => allUrls.toSeq)
226+ }
227+
228+ def verifySnippets (migratedSnippetUrls : Seq [String ]): Future [Unit ] = {
229+ pprint.pprintln(" Verifying snippets..." )
230+
231+ var count = 0L
232+ var progress = 0
233+ var errors = 0
234+
235+ def step (): Future [Unit ] = {
236+ if (count >= migratedSnippetUrls.size) {
237+ pprint.pprintln(s " Verification complete! Total errors: $errors" )
238+ Future .unit
239+ } else {
240+ val batch = migratedSnippetUrls.slice(count.toInt, (count + 100 ).toInt)
241+
242+ Future .traverse(batch) { url =>
243+ val snippetId = newApi.SnippetId .fromString(url)
244+
245+ for {
246+ oldOpt <- {
247+ oldSnippets
248+ .find(Filters .eq(" simpleSnippetId" , url))
249+ .headOption()
250+ .map(_.flatMap(fromBson[oldStorage.MongoSnippet ]))
251+ }
252+ newOpt <- Future .successful(pgContainer.readPostgresSnippet(snippetId))
253+ } yield (url, oldOpt, newOpt)
254+ }.flatMap { results =>
255+ results.foreach { case (url, oldOpt, newOpt) =>
256+ (oldOpt, newOpt) match {
257+ case (Some (old), Some (newS)) =>
258+ val oldCode = old.inputs.code.replace(" import com.olegych" , " import org.scastie" )
259+ val newCode = newS.inputs match {
260+ case sbt : newApi.SbtInputs => sbt.code
261+ case cli : newApi.ScalaCliInputs => cli.code
262+ }
263+
264+ if (oldCode != newCode) {
265+ errors += 1
266+ pprint.pprintln(s " ❌ Snippet $url mismatch " )
267+ } else {
268+ pprint.pprintln(s " ✓ Snippet $url verified successfully " )
269+ }
270+
271+ case (None , Some (_)) =>
272+ errors += 1
273+ pprint.pprintln(s " ❌ Snippet $url missing in old database " )
274+
275+ case (Some (_), None ) =>
276+ errors += 1
277+ pprint.pprintln(s " ❌ Snippet $url missing in new database " )
278+
279+ case (None , None ) =>
280+ errors += 1
281+ pprint.pprintln(s " ❌ Snippet $url missing in both databases " )
282+ }
283+ }
284+
285+ count += batch.size
286+ val cProgress = (count * 100 ) / migratedSnippetUrls.size
287+ if (cProgress > progress) {
288+ progress = cProgress.toInt
289+ pprint.pprintln(s " Verification Progress: [ $cProgress% / 100%] " )
290+ }
291+
292+ step()
293+ }
294+ }
295+ }
296+
297+ step()
298+ }
299+
300+ try {
301+ val snippetsNumber = Await .result(oldSnippets.countDocuments().head, Duration .Inf )
302+
303+ pprint.pprintln(s " Starting migration of $snippetsNumber snippets... " )
304+
305+ val migrationFuture = for {
306+ migratedUrls <- migrateSnippets(snippetsNumber.toInt)
307+ _ <- verifySnippets(migratedUrls)
308+ } yield ()
309+
310+ Await .result(migrationFuture, Duration .Inf )
311+
312+ } catch {
313+ case t : Throwable => t.printStackTrace()
314+ } finally {
315+ mongoClient.close()
316+ }
317+ }
0 commit comments