-
-
Notifications
You must be signed in to change notification settings - Fork 27
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
Migration? #15
Comments
Has a new idea that could be used to migrations... |
I think migrations should be their own thing, especially since migrations are done at the Model level, not the collection one. Collection versioning is a great idea, though! |
Maybe best explained, Migrations are an operation between two versions of the same model. So how would that best work? Maybe have something that encapsulates models like a collection, but it is NOT a collection of models, but of conversion operations. A big problem would be naming conventions. Say we have two versions of model A. How would a migration of A:ver<1.2> to A:ver<1.3> be named? Is something like this possible? migration A:from<1.2>:to<1.3> { ... } If so, I have an idea to do this in a very clean manner. |
no, it isn't, I think... what about: migration A:ver<1.3> {
method from:ver<1.2> {...}
} |
Actually, that's not a bad idea. However I was hoping to use methods for individual fields. In that case, field level conversions could use another mechanism. Try this: # Pseudo
migration A:ver<1.3> {
# Specific field-level conversions. Key layout is:
# <from_version><model><attribute> => -> $old_model, $to_model { ... }
has %!conversions;
has CollectionA<1.2> $!old_a;
has CollectionA<1.3> $!new_a;
method from:ver<1.2> {
# Iterate over each model.
for $!old_a.^models Z $!new_a.^models -> ($old_model, $new_model) {
for $new_model.^attributes -> $attr {
with %!conversion<1.2>{$new_model}{$attr} {
$_($old_model, $new_model)
} else {
# By default, we move the value from the old model to the new.
$new_model."&{ $attr }"() = $old_model."&{ $attr }"();
}
}
}
}
} |
Note that usually DBMSs provide non-standard SQL syntaxes for migrations, like:
And some databases allow to run CREATE/ALTER/DROP in transactions. I believe that it would be nice to make use of these features, where available. For example the migration itself could have an on_conflict property that could be "ignore" (if not exists), "replace" (or replace), "fail". |
I were wondering about migration and I think I came to something interesting... If I have 2 versions of the same model, for example: model Bla:ver<0.1> {
has Int $.a is column;
}
model Bla:ver<0.2> {
has Str $.b is column;
} It’s relatively easy to say that it should create a column b of type string and drop the column a. The problem is try to guess what should be done with the data... if the content of be shoul be generated based on the old data on a, we have a problem, once we dropped a. We could fix that explaining to the migration how to generate the data. The other migrations that I know manipulate the data using plain SQL. But we already have a way to manipulate data! The AST! I don’t think it would be impossible to make something like this to generate the data for a new column: method #`{or sub, idk} migrate:<a> {
“String: { .a * 3 }”
} And it would run a: UPDATE
my_table
SET
b = ‘String: ‘ || a * 3 Or something that would be better for that migration (@Santec, please help me!) Maybe something should be a bit different because it is possible to a new column on a table can use data from different tables. Sent with GitHawk |
@FCO: This is why I had the %!conversions attribute. So for something like this situation, you'd have submethod BUILD {
%!conversions<0.2><Bla><b> = -> { $new_model<Bla>.b = $old_model<Bla>.a };
} So %!conversions handles all special casing at the field level. |
Do we need the new model? Won’t we always use only the old one? What about? migration MySchema:ver:<0.2> {
has Bla:ver<0.1> $.old-model1;
has Ble:ver<0.1> $.old-model2;
has Bla:ver<0.2> $.new-model1;
has Ble:ver<0.2> $.new-model2;
method Bla:<a> { “{ $!old-model1.b } & { $!old-model2.c }” }
} And it would use the return to create the update... Sent with GitHawk |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Now I see that it doesn't make sense to use another table here... I don't have a join here... it should be done with relationship... |
So maybe it make sense to have migrations by model... |
Now I think I got it! Bla.^migration: :from<0.1>, {
.a = .b * 42 + 3;
.c = .d - .e;
} |
I just started playing with migrations... now (since a185e2f) it's possible:
please, pay attention on |
This comment has been minimized.
This comment has been minimized.
any thoughts about it? |
Yes. That;'s not bad. It accomplishes the basics. However I would prefer if we did migration as a ClassHOW so that we can split complex operations up into encapsulated (self-contained) pieces. You can still do it with this method, but everything has to be written out. Give me a few days to think about ways alleviate this issue and if I find any, I will post. |
The solution I started implementing do not handle:
Maybe a solution with a migration type with a collection of models could help with it... (My next step is creating the migration models to save the state of a migration on the database...) |
I like the option of Red generate the SQL and the developer be able to review/edit it. But is the advantage of the yaml instead of saving it directly on Red's syntax? |
There is no specific advantage to YAML. They use it because it is convenient. The YAML is used in order to accurately diff the two versions of the schema. They convert the schema to an internal format which makes it easier to diff. The YAML is simply a serialisation of this data structure. This helps, because it means you can jump directly to this format for the previous version, instead of trying to figure it out from the code. Note that the previous version of the schema probably only exists in git. Therefore, they put it in the project directory for later! Any format will do. |
On Red we can use objs of this types. They were made with that content in mind. |
That would be super. Here's my proposal then.
How is this sounding so far? |
Why store the actual version of the database schema? Why not just compare tour classes with the current database schema? |
You need to be able to run all the migrations between version X and the latest, because migrations might not simply be to update the schema; they might have data migrations in between as well. Therefore, you need to be able to discover what X is. You might as well store a serialised version of the data structures used for comparing, essentially as a cache and a historical record. |
I just thought a way that could work that I think would be perfect to my way of working I'm not sure if it would be good to everyone, but I'll describe it here and please let me know what you think. We may generalise that... The user creates the models the way they like, then run the command:
it will update the DB to reflect the models, read the DB after the changes, generate and save Red::Migration::* objects on a after that, when the user is sure that's how the DB should look, they can run:
it will create 2 new files then on the target system, the user can run
it will create the DB using the SQL on When the user wants to change something it edits the models and then run:
it will validate if the DB is still in sync with the Red::Migration::* objects stored on The user can create new changes and run When the use is happy, they can run:
that will get the diff between the last version on Running on the target system:
Will run the
Would run it should also be possible to run:
that would do the diff and also include on SQL something like: update my_model set new_column = old_col1 || " " || old_col2; -- probably doing that on batches, but you got it... it should also be possible to run something like:
and that would apply the SQL on the local database and then read it to create the Red::Migration::* objects on After a After a The generated SQL files may need SHA1 to know if they were changed (it could be stored on The dir where we store the SQL should also be different fo what driver we are using... When validating if the DB is still in sync, if that's not, it could/should be possible to generate a intermediate version (if an option is passed, something like when running Please let me know if there are any comments/suggestions... |
of course all command/sub-command names can be changed... |
As Voldenet suggested on #raku irc channel, it can be used by many different on git... maybe, instead of using ints for path on migrations, we could use UUID to avoid merge conflicts, but we would need to find another way to decide order. |
I think there should be migration v2 {
method created-on { "2024-03-07T23:42:03.430480Z" } # used for order of migrations
# method using high-level language of operations to perform
method update {
.remove-column(:table<user> :name<type>);
.create-table({
.set-name('admin');
# Guid will get converted into uniqueidentifier, char(36) or something supported by current db
.add-column(:type(Guid) :name<userId>);
});
}
# method describing changes to data model, this lets Red assume what this migration does
# even if performed operations are different
method Red-update {
.remove-column(:table<user> :name<type>);
.create-table({
.set-name('admin');
.add-column(:type(Guid) :name<userId>);
});
}
method revert { ... }
method Red-revert { ... }
} After the migration is generated and applied to current database, user can review this high-level-language change, reorder it and replace the scripting (in the example, users stop having "type" indicating admins and get moved into table containing admins): migration v2 {
method created-on { "2024-03-07T23:42:03.430480Z" } # used for order of migrations
method upgrade {
# user can reorder schema modifications
.create-table({
.set-name('admin');
# user can choose data type explicitly, which is useful for floats of different precision
.add-column(:type("uniqueidentifier") :name<userId>);
});
# user can add sql code to move admins to the new table
.sql('insert into admin(userId) select id from user where type = 1');
# user can choose to risk not removing the column,
# in which case Red would assume, that this column is considered as removed
# .remove-column(:table<user> :name<type>);
}
# this mustn't change
method Red-upgrade {
.remove-column(:table<user> :name<type>);
.create-table({
.set-name('admin');
.add-column(:type(Guid) :name<userId>);
});
}
method revert { ... }
method Red-revert { ... }
} With the above code, CREATE TABLE admin ([userId] uniqueidentifier);
insert into admin(userId) select id from user where type = 1;
INSERT INTO [$migrations]([Name], [Date]) VALUES ('v2', SYSDATETIME()); |
I'm not sure if I got the |
A few notes regarding my previous post:
|
Regarding The |
if we are going that path, I was thinking about that "class"... I am thinking on something like this: migration CreateAdminDropTypeFrmUser:ver<2> {
method up {
User.^migration: *.drop-column("type");
Schema.^migration: { # There is no model for that, then we need use schema
.create-table("admin", { userId => %( :type<Guid> ) });
});
}
method down {
User.^migration: *.create-column("type", :type<text>); # maybe, when deleting on the local DB
# we could store the data somewhere in case we
# want undo it (this down method)
Schema.^migration: *.drop-table: "admin"; # again, no model, so we need to use schema
}
} |
That makes sense |
The last example is nice, would require keeping versioned entities ( class Schema:ver<2> does RedSchema { has @.models = ("Model1:ver<1>", "Model2:ver<3>", "Model3:ver<5>") } and then having |
Each migration dir may have a Schema.rakumod file where will have: use RedSchema;
red-schema "Model1:ver<1>", "Model2:ver<3>", "Model3:ver<5>"; and on migration file we would |
No, it would need to be something like: use RedSchema ("Model1:ver<1>", "Model2:ver<3>", "Model3:ver<5>"); |
I've been wondering... would it make sense to on updating local db, save the dump of the DB to be able to restore it in case the user want to revert it? |
We will probably need our own version of META6.provides. A file can contain multiple models. And maybe we shouldn't use the model version as path to avoid merge conflicts... |
Should we have a different module called Red::Migration instead of doing that inside Red? |
We'll probably need a configuration file... should we use Configuration? |
Not sure if we are going to use Configuration, but I'm here just thinking out loud what we will probably need: class MigrationConfig {
has UInt $.current-version = self!find-current-version;
has UInt %.model-current-version;
has IO() $.base-path where *.add("META6.json") ~~ :f = $*CWD;
has IO() $.migration-base-path = $!base-path.add: "migrations";
has IO() $.dump-path = $!base-path.add: ".db-dumps";
has IO() $model-storage-path = $!migration-base-path.add: "models";
has IO() $.version-storage-path = $!migration-base-path.add: "versions";
has Str() $.sql-subdir = "sql";
has Str() @.drivers = <SQLite Pg>;
has %!versions-cache = self!compute-version-cache;
has %!models-cache = self!compute-model-cache;
method !compute-version-cache {...}
method !compute-models-cache {...}
method !find-current-version {...}
method !random-string {...}
multi method version-path($version) {
$!version-storage-path.add: ...
}
multi method version-path {
$.version-path: $!current-version
}
method new-model-version(Str $model-name) {
++%!current-model-version{$model-name}
}
multi method model-path(Str() $model-name) {
$.model-path: $model-name, %!model-current-version{$model-name}
}
multi method model-path(Str() $model-name, UInt $version) {
$.model-version-path: $model-name, $version
}
multi method model-version-path(Str() $model-name, UInt() $version) {
$!model-storage-path.add: %!models-cache{$model-name}{$version}
}
multi method model-version-path(Str() $model-name-version) {
if $model-name-version ~~ /^$<name>=[<[\w:]>*\w] ":" ["ver<" ~ ">" $<ver>=[\d+]|\w+ "<" ~ ">" \w+]* % ":"/ {
$.model-version-path: $<name>, $<ver> // 0
}
multi method migration-sql-path(Str $driver where @!drivers.one) {
$.version-path.add($!sql-subdir).add: $driver
}
multi method migration-sql-path(Str $driver where @!drivers.one, UInt $version) {
$.version-path($version).add($!sql-subdir).add: $driver
}
} |
We could use Configuration for this, based on what I'm looking at, I like where this is going |
The way I'm seeing it now is:
|
Use
:ver<>
?The text was updated successfully, but these errors were encountered: