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
WIP: SQLite3 support for migrations #221
Conversation
5eb1f21
to
fe7eecd
Compare
@a8m at the risk of spamming you, here are some highlights in the existing work you should take note of. I have the FK work to do still and a few tests, but otherwise I think everything is functioning. Note that due to how sqlite3 lib handles parameters, I don't think using a https://github.com/facebookincubator/ent/pull/221/files#diff-39e4b302014280cdc713d8c1a32acb2eR140 -- this tinyint(1) special case for mysql is perpetuated here, do we want that? https://github.com/facebookincubator/ent/pull/221/files#diff-56a4aef6402a64fbe636f02a23d8fa57R22 better place for this test? also the following refactors were made: https://github.com/facebookincubator/ent/pull/221/files#diff-932a340b0ff6f22c5b5a59ce1e8a4972R434 - first function is taken from mysql driver and the second is taken from pg, these are just utility functions to perform standard operations I needed. I hope the moves are ok. |
Additionally, it doesn't seem like constraints are uniquely named throughout sqlite3, leading to an issue i've pushed here: you can see there is no globally unique identifier. what matches against the name provided to
I'm committing some code now that shows my problem; read the fkexist method. Thanks again for your patience with me. :) |
7403e91
to
8ac4326
Compare
Hey @erikh, thanks for working on this! |
No rush; I'll keep chipping away at it.
…On Fri, Dec 6, 2019 at 9:50 AM Ariel Mashraki ***@***.***> wrote:
Hey @erikh <https://github.com/erikh>, thanks for working on this!
I'll review this on Sunday (weekend in Israel is Friday–Saturday).
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#221>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAET223LVDTNSMXT3THRC3QXKGGNANCNFSM4JWDFFKQ>
.
|
You're not spamming at all. Thanks for working on this.
I think
I don't think so. I think it's OK to support only
Let's change the method then to: fkExist(ctx context.Context, tx dialect.Tx, table, fk string) (bool, error) Let me know if you plan to break it to smaller PRs, or I'll just review it. |
I'll probably put more in on this today. This might be up for a while as I
continue to chip on it; hope that's ok. Thanks for the feedback!
…On Sun, Dec 8, 2019 at 9:06 AM Ariel Mashraki ***@***.***> wrote:
@a8m <https://github.com/a8m> at the risk of spamming you, here are some
highlights in the existing work you should take note of. I have the FK work
to do still and a few tests, but otherwise I think everything is
functioning.
You're not spamming at all. Thanks for working on this.
If you prefer to work with smaller PRs (like, only table scanning and them
reading indexes, etc..) that's fine too.
Note that due to how sqlite3 lib handles parameters, I don't think using a
? or binding syntax will work in the pragmas. I think since we're working
totally on trusted input that a sprintf is safe, but I would like to hear
your input.
I think Sprintf is fine here.
https://github.com/facebookincubator/ent/pull/221/files#diff-39e4b302014280cdc713d8c1a32acb2eR140
-- this tinyint(1) special case for mysql is perpetuated here, do we want
that?
I don't think so. I think it's OK to support only bool and boolean,
because these are the common types in the ORMs outside (feel free to
correct me if I have a wrong assumption).
https://github.com/facebookincubator/ent/pull/221/files#diff-56a4aef6402a64fbe636f02a23d8fa57R22
better place for this test?
entc/integration/migrate/migrate_test.go
you can see there is no globally unique identifier. what matches against
the name provided to fkExist?
Let's change the method then to:
fkExist(ctx context.Context, tx dialect.Tx, table, fk string) (bool, error)
------------------------------
Let me know if you plan to break it to smaller PRs, or I'll just review it.
Thanks again for your contribution.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#221>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAET2ZHITGXFTQNCYOZHLLQXUSSRANCNFSM4JWDFFKQ>
.
|
I'm in process of porting the postgres table migration tests over to sqlite3 but your other change suggestions have been implemented. |
ok the migration tests are up as well as some related fixes. I'll fix up any tests next, but what should I be doing with this patch after this? writing more tests? |
how do I safely run the integrations? is there a doc for it? |
nvm found it |
https://www.sqlite.org/lang_altertable.html#otheralter is going to be necessary for the FK moves and other operations on constraints such as changing the on delete or update triggers. Where would this code best live? |
Maybe I miss something, but why do we need to create a temporary table if we can create FKs as mentioned in #200 (comment)? |
it's when FKs need to be migrated or altered that it'll be an issue. if you
are not adding the column -- for example adding a foreign key to an
existing column -- you have to perform this operation.
…On Wed, Dec 11, 2019 at 11:49 AM Ariel Mashraki ***@***.***> wrote:
https://www.sqlite.org/lang_altertable.html#otheralter is going to be
necessary for the FK moves and other operations on constraints such as
changing the on delete or update triggers.
Maybe I miss something, but why do we need to create a temporary table if
we can create FKs as mentioned in #200 (comment)
<#200 (comment)>
?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#221>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAET23WBZNIPJIKBX4745LQYE74PANCNFSM4JWDFFKQ>
.
|
Currently, a FK is added with its column to the schema file, when a new edge is added to the We can add support for this (I don't really mind), but I think it adds much more complexity for a feature/functionality that we don't really need. Let me know what do you think. |
Ok yeah, if you don't think we'll need it I'll review and make sure none of
our tests require it... I think that's where I got blocked last time I
checked in on it.
I'll review later tonight and get back to you, on another project atm.
…On Wed, Dec 11, 2019 at 12:32 PM Ariel Mashraki ***@***.***> wrote:
for example adding a foreign key to an existing column -- you have to
perform this operation.
Currently, a FK is added with its column to the schema file, when a new
edge is added to the ent.Schema. Meaning, we don't have situations that a
FK is added to a column that already exists in the table.
We can add support for this (I don't really mind), but I think it adds
much more complexity for a feature/functionality that we don't really need.
Let me know what do you think.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#221>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAET2665CIDVNSYJUGSBMTQYFE6PANCNFSM4JWDFFKQ>
.
|
Sorry, I haven't had time to get to this over the week. I'll try to polish this off next week. |
519175d
to
7d06b42
Compare
This should pass all tests now; I've squashed the patch and I think it's ready for at least an initial review. I'm sure there are still some things outstanding. |
some notes:
|
one last thing; I just don't know if this is going to behave in the following scenario:
This is probably not a problem for very simple situations but could be in the future. |
7d06b42
to
e75a14c
Compare
e75a14c
to
b4f476b
Compare
@a8m what should I do with this patch? |
Hey @erikh, sorry for the delay. I missed the previous messages. I'll review this later today (or tomorrow). |
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.
Great work @erikh!
I've added a few comments.
Most of them are minor, but the major one should be handled (or we can discuss about it).
Update: "major" => first comment below.
if t.Dialect() == dialect.SQLite { | ||
t.Queries = append(t.Queries, &Wrapper{"ADD COLUMN %s", fk}) | ||
} else { | ||
t.Queries = append(t.Queries, &Wrapper{"ADD CONSTRAINT %s", fk}) | ||
} |
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.
IMO this logic should reside in the schema migration package.
I'm thinking about 2 options (but you're welcome to suggest other alternatives):
- Add an
addFK
method to thesqlDialect
interface that generates a different command for thesqlite3
dialect. - Change
fkExist
logic insqlite3
to always returnstrue
(as it was before), and append the foreign-key-clause to the new column we create. For example:ALTER table t1 ADD COLUMN c1 int REFERENCES t2(c2) ON DELETE SET NULL;
Let me know what do you think about it.
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.
ok, that makes sense. I think I get the general gist of what your'e suggesting:
func (s *SQLite) addFK(t *Table, *fk ForeignKey) string {
// construct string to add foreign key
}
func (s *SQLite) fkExist(t *Table, *fk ForeignKey) bool {
return true
}
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.
@a8m ok after some noodling on this (and re-reading what you said, multiple times, sorry about missing the message there!):
-
addFK leads to import cycles best I can see without creating a third package, but I'm still playing around with this approach and it might prove fruitful. I can't share the migrate package with the builder directly because of the relationship that already exists. I'm guessing I'm missing something here and will report back after more searching around.
-
setting things to true works fine, but then the alter cannot work at all unless I am still misunderstanding? I'm not sure is the best approach to adding edges to existing tables. I've implemented most of this and the clear problem with it is that it will never work on the second migration to the same table as the user probably would expect (with fks populated).
I think what might make sense here (but need to examine a little more) is altering the addColumn interface method to accept a foreignkey struct, or otherwise relate the foreign keys to the columns being currently built (which is true for FKs but not the columns that they are related to). This way I can deliberately say "I want to ensure my foreign key relationships are always added as a part of my column mutations to tables, no other way". And I think, but am not 100% certain, this way the work in fk.DSL() can either be simplified or removed for SQLite's case and we can just match/query the foreign keys directly in the Column portion of the migration in the tBuilder method and simplify the building process here.
WDYT?
Also sincerely sorry for the uber-high latency on this patch, I'll try to button things up this week.
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 think what might make sense here (but need to examine a little more) is altering the addColumn interface method to accept a foreignkey struct, or otherwise relate the foreign keys to the columns being currently built (which is true for FKs but not the columns that they are related to).
I think that's fine (as I mentioned in #221 (comment)). If you want to go with this approach, I can help with that and add the code that links the columns with their foreign-keys.
WDYT?
if fk.Dialect() == dialect.SQLite { | ||
fk.Pad().Ident(fk.columns[0]) | ||
} else { | ||
fk.WriteString("FOREIGN KEY") | ||
fk.Nested(func(b *Builder) { | ||
b.IdentComma(fk.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.
ditto
|
||
if new == nil { | ||
return nil, errors.New("determined state could not be determined during change set generation") | ||
} |
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.
Use this format: sql/schema: determined state could ...
.
Also, maybe add the table name as well?
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.
ok. Would you like wrappers for these package name identifiers usable for github.com/pkg/errors.Wrap
? Could do this in a later PR.
return r == '(' || r == ')' || r == ' ' || r == ',' | ||
}); parts[0] { | ||
|
||
switch parts := typeFields(c.typ); parts[0] { |
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.
nice!
c.Type = field.TypeJSON | ||
case "uuid": | ||
c.Type = field.TypeUUID | ||
case "enum": |
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 don't think this clause is needed.
mock.ExpectQuery(escape("pragma foreign_key_list('users')")). | ||
WillReturnRows(sqlmock.NewRows([]string{"id", "seq", "table", "from", "to", "on_update", "on_delete", "match"})) | ||
mock.ExpectExec("ALTER TABLE `users` ADD COLUMN `spouse_id` REFERENCES `users`\\(`id`\\) ON DELETE CASCADE"). | ||
WillReturnResult(sqlmock.NewResult(0, 1)) |
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.
Great testing work!
} | ||
} | ||
|
||
return false, rows.Close() |
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.
ditto
ok. it's a holiday here in the US so I'll be addressing this later this
week; thanks for the review!
…On Tue, Dec 24, 2019 at 5:57 AM Ariel Mashraki ***@***.***> wrote:
***@***.**** commented on this pull request.
Great work @erikh <https://github.com/erikh>!
I've added a few comments.
Most of them are minor, but the major one should be handled (or we can
discuss about it).
------------------------------
In dialect/sql/builder.go
<#221 (comment)>:
> + if t.Dialect() == dialect.SQLite {
+ t.Queries = append(t.Queries, &Wrapper{"ADD COLUMN %s", fk})
+ } else {
+ t.Queries = append(t.Queries, &Wrapper{"ADD CONSTRAINT %s", fk})
+ }
IMO this logic should reside in the schema migration package.
I'm thinking about 2 options (but you're welcome to suggest other
alternatives):
1. Add an addFK method to the sqlDialect interface that generates a
different command for the sqlite3 dialect.
2. Change fkExist logic in sqlite3 to always returns true (as it was
before), and append the foreign-key-clause
<https://sqlite.org/syntax/foreign-key-clause.html> to the new column
we create. For example:
ALTER table t1 ADD COLUMN c1 int REFERENCES t2(c2) ON DELETE SET NULL;
Let me know what do you think about it.
------------------------------
In dialect/sql/builder.go
<#221 (comment)>:
> + if fk.Dialect() == dialect.SQLite {
+ fk.Pad().Ident(fk.columns[0])
+ } else {
+ fk.WriteString("FOREIGN KEY")
+ fk.Nested(func(b *Builder) {
+ b.IdentComma(fk.columns...)
+ })
+ }
+
ditto
------------------------------
In dialect/sql/schema/migrate.go
<#221 (comment)>:
> @@ -235,6 +236,14 @@ type changes struct {
// changeSet returns a changes object to be applied on existing table.
// It fails if one of the changes is invalid.
func (m *Migrate) changeSet(curr, new *Table) (*changes, error) {
+ if curr == nil {
+ return nil, errors.New("current state could not be determined during change set generation")
+ }
+
+ if new == nil {
+ return nil, errors.New("determined state could not be determined during change set generation")
+ }
Use this format: sql/schema: determined state could ....
Also, maybe add the table name as well?
------------------------------
In dialect/sql/schema/mysql.go
<#221 (comment)>:
> @@ -240,9 +240,8 @@ func (d *MySQL) scanColumn(c *Column, rows *sql.Rows) error {
if nullable.Valid {
c.Nullable = nullable.String == "YES"
}
- switch parts := strings.FieldsFunc(c.typ, func(r rune) bool {
- return r == '(' || r == ')' || r == ' ' || r == ','
- }); parts[0] {
+
+ switch parts := typeFields(c.typ); parts[0] {
nice!
------------------------------
In dialect/sql/schema/sqlite.go
<#221 (comment)>:
> + c.Size = math.MaxInt32
+ c.Type = field.TypeString
+ case "varchar":
+ c.Type = field.TypeString
+ if len(parts) > 1 {
+ size, err := strconv.ParseInt(parts[1], 10, 64)
+ if err != nil {
+ return fmt.Errorf("converting varchar size to int: %v", err)
+ }
+ c.Size = size
+ }
+ case "json":
+ c.Type = field.TypeJSON
+ case "uuid":
+ c.Type = field.TypeUUID
+ case "enum":
I don't think this clause is needed.
------------------------------
In dialect/sql/schema/sqlite.go
<#221 (comment)>:
> @@ -119,6 +123,52 @@ func (*SQLite) cType(c *Column) (t string) {
return t
}
+func (d *SQLite) typeField(c *Column, str string) error {
+ switch parts := typeFields(str); parts[0] {
+ case "int", "integer":
+ c.Type = field.TypeInt32
+ case "smallint":
+ c.Type = field.TypeInt16
+ case "bigint":
+ c.Type = field.TypeInt64
+ case "tinyint":
+ c.Type = field.TypeInt8
+ case "double", "real":
+ c.Type = field.TypeFloat64
+ case "timestamp", "datetime":
"timestamp"
What is usually used by other ORMs? I know that date and datetime types
exist in sqlite, but not familiar with timestamp.
------------------------------
In dialect/sql/schema/sqlite_test.go
<#221 (comment)>:
> + mock.ExpectQuery(escape("SELECT COUNT(*) FROM `sqlite_master` WHERE `type` = ? AND `name` = ?")).
+ WithArgs("table", "users").
+ WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
+ mock.ExpectQuery(escape(`pragma table_info('users')`)).
+ WillReturnRows(sqlmock.NewRows([]string{"id", "name", "type", "not_null", "dflt", "pk"}).
+ AddRow(0, "id", "bigint", 1, nil, 1).
+ AddRow(1, "name", "varchar", 0, nil, 0))
+ mock.ExpectQuery(escape("pragma index_list('users')")).
+ WillReturnRows(sqlmock.NewRows([]string{"seq", "name", "unique", "origin", "partial"}).
+ AddRow(0, "sqlite_autoindex_users_1", 1, "pk", 0))
+ mock.ExpectExec(escape("ALTER TABLE `users` ADD COLUMN `spouse_id` integer NULL")).
+ WillReturnResult(sqlmock.NewResult(0, 1))
+ mock.ExpectQuery(escape("pragma foreign_key_list('users')")).
+ WillReturnRows(sqlmock.NewRows([]string{"id", "seq", "table", "from", "to", "on_update", "on_delete", "match"}))
+ mock.ExpectExec("ALTER TABLE `users` ADD COLUMN `spouse_id` REFERENCES `users`\\(`id`\\) ON DELETE CASCADE").
+ WillReturnResult(sqlmock.NewResult(0, 1))
Great testing work!
------------------------------
In dialect/sql/schema/mysql.go
<#221 (comment)>:
> @@ -240,9 +240,8 @@ func (d *MySQL) scanColumn(c *Column, rows *sql.Rows) error {
if nullable.Valid {
c.Nullable = nullable.String == "YES"
}
- switch parts := strings.FieldsFunc(c.typ, func(r rune) bool {
- return r == '(' || r == ')' || r == ' ' || r == ','
- }); parts[0] {
+
⬇️ Suggested change
-
------------------------------
In dialect/sql/schema/migrate.go
<#221 (comment)>:
> @@ -235,6 +236,14 @@ type changes struct {
// changeSet returns a changes object to be applied on existing table.
// It fails if one of the changes is invalid.
func (m *Migrate) changeSet(curr, new *Table) (*changes, error) {
+ if curr == nil {
+ return nil, errors.New("current state could not be determined during change set generation")
+ }
+
⬇️ Suggested change
-
------------------------------
In dialect/sql/schema/sqlite.go
<#221 (comment)>:
> + var (
+ id int
+ seq int
+ tableName string
+ from string
+ to string
+ onUpdate string
+ onDelete string
+ match string
+ )
+ if err := rows.Scan(&id, &seq, &tableName, &from, &to, &onUpdate, &onDelete, &match); err != nil {
+ return false, errors.Wrap(err, "while querying foreign keys")
+ }
+
+ if tableName == fk.RefTable.Name && fk.Columns[0].Name == from && fk.RefColumns[0].Name == to {
+ return true, rows.Close()
Why not:
⬇️ Suggested change
- return true, rows.Close()
+ return true, nil
You already call defer rows.Close() above.
------------------------------
In dialect/sql/schema/sqlite.go
<#221 (comment)>:
> + from string
+ to string
+ onUpdate string
+ onDelete string
+ match string
+ )
+ if err := rows.Scan(&id, &seq, &tableName, &from, &to, &onUpdate, &onDelete, &match); err != nil {
+ return false, errors.Wrap(err, "while querying foreign keys")
+ }
+
+ if tableName == fk.RefTable.Name && fk.Columns[0].Name == from && fk.RefColumns[0].Name == to {
+ return true, rows.Close()
+ }
+ }
+
+ return false, rows.Close()
ditto
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#221>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAET22AI2G6ELEUYZ3WLLLQ2IINRANCNFSM4JWDFFKQ>
.
|
Thanks for the review. I'll try to turn this around over the weekend. I assume I probably won't hear from you until after the new year. ;) Enjoy your time. |
b4f476b
to
da2f446
Compare
I haven't forgotten about this; just haven't had much time this week. Thanks and sorry. |
I can help you with that if you want. Just split it to multiple PRs, so we can merge all the ready parts. |
Work in progress until this message is removed Signed-off-by: Erik Hollensbe <github@hollensbe.org>
d50f092
to
5b3a31c
Compare
Between a variety of personal conflicts I can't really spend too much more time on this, and I feel bad just leaving it here. Happy to split it up over the next week if you want to take pieces of it, or you can cherry-pick what you would like to keep. I really do need this behavior though, so I'd encourage you to persist with the features this patch was intended to deliver. Sorry man. I hate doing this to people but life is just getting too busy. |
Let me know how you want it split up and I will do the work. |
How much time will it take to merge this pr? This feature is important for me, i'm using ent instead of xorm. |
now this pr needs to be fixed as it have merge conflicts, but yes, how we can help to get this merged? |
Thanks for the great work @erikh, and sorry for my delayed response. I'll check if I can split (and edit) it by myself and merge parts of this to Thanks again and don't feel bad about it! |
Don't sweat credit. I appreciate it, but if the code isn't fitting well
into the current state of things, just scrap it.
Discovery notes from working on this:
I think the biggest problem with ent and sqlite revolve around the foreign
key migrations, it's going to lead to some surprising behavior as currently
implemented, when the keys just don't get managed in the destructive
migration types (the state of this patch). I would strongly suggest
implementing the table replacement procedure that sqlite specifies;
realistically, shared instances of sqlite are not going to happen often
enough for concurrency to be an issue, and I don't think most production
uses are going to be sqlite-backed anyway. For a solution though, some
other gate keeper could suffice as an out-of-band lock for controlling the
access at the right time when those situations do come, but there's no need
to one-shot a patch to this behavior. I think most sqlite users will be
satisfied with being able to migrate at all, and I don't think most of them
are using shared sqlite databases, but that's why I'm posting this here. :)
Anyways hope the flavor is welcome, and sorry I haven't had time. I'm
between a lot of things personally and I just kept putting it off because
other things were taking control, and it seems like a foregone conclusion
now. I hope there's no frustration on the lack of delivery.
Thanks,
@erikh
…On Tue, Feb 11, 2020 at 4:20 AM Ariel Mashraki ***@***.***> wrote:
Thanks for the great work @erikh <https://github.com/erikh>, and sorry
for my delayed response.
I'll check if I can split (and edit) it by myself and merge parts of this
to ent. I want to make sure you get the credit for your work.
Thanks again and don't feel bad about it!
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#221>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAET2ZLXICUXUE7BVCH7Y3RCKJYVANCNFSM4JWDFFKQ>
.
|
This is a WIP PR and should be ignored this moment. It's based on PR #221 created by Erik Hollensbe (He should get his credit for his work before we land this).
This is a WIP PR and should be ignored this moment. It's based on PR #221 created by Erik Hollensbe (He should get his credit for his work before we land this).
This is a WIP PR and should be ignored this moment. It's based on PR #221 created by Erik Hollensbe (He should get his credit for his work before we land this).
This is a WIP PR and should be ignored this moment. It's based on PR #221 created by Erik Hollensbe (He should get his credit for his work before we land this).
This is a WIP PR and should be ignored this moment. It's based on PR #221 created by Erik Hollensbe (He should get his credit for his work before we land this).
This is a WIP PR and should be ignored this moment. It's based on PR #221 created by Erik Hollensbe (He should get his credit for his work before we land this).
This is a WIP PR and should be ignored this moment. It's based on PR #221 created by Erik Hollensbe (He should get his credit for his work before we land this).
#428 was merged and it adds the first basic migration functionality for SQLite (adding resources). I want to thank you @erikh for this amazing work and for being active and involved in this project! |
This is a basic start, a lot of cut and paste but it does pass some very basic tests now. I still have more work to do but I was hoping we could discuss some of it as it arrives, so here's the PR.