Skip to content

Conversation

@nazrhom
Copy link
Contributor

@nazrhom nazrhom commented Oct 20, 2017

Added support for automatic migrations between two schemas.

Supported auto-migrations are: adding or removing terms, both fields and tables, renaming will only be performed when removing a synonym or synonymous form.

@resin-io-modules-versionbot
Copy link
Contributor

@nazrhom, status checks have failed for this PR. Please make appropriate changes and recommit.

@nazrhom nazrhom requested review from CameronDiver and Page- October 20, 2017 14:58
@Page- Page- requested a review from dfunckt October 23, 2017 22:17
migration = AbstractSQLCompiler.postgres.diffSchemas(srcSchema, dstSchema)
console.log('GOT MIGRATION', migration)
catch e
throw e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might as well drop the try/catch altogether if you're just going to rethrow :P

catch e
throw e
for i in [0...Math.max(migration.length, expectation.length)]
expect(migration[i]).to.equal(expectation[i])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be able to be

expect(migration).to.deep.equal(expectation)

and avoid the need for the loop

dstSchema = LF2AbstractSQLTranslator(dstLf, 'Process')

migration = AbstractSQLCompiler.postgres.diffSchemas(srcSchema, dstSchema)
console.log('GOT MIGRATION', migration)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug info?

''', [ '''
ALTER TABLE "pilot-can fly-plane"
RENAME TO "plane-can be flown by-pilot";
RENAME COLUMN "pilot" TO "can be flown by-pilot";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure this is invalid and should be

ALTER TABLE "plane-can be flown by-pilot"
RENAME COLUMN "pilot" TO "can be flown by-pilot";

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first instruction is there to rename the the table representing the relation. I used https://www.postgresql.org/docs/9.1/static/sql-altertable.html for reference but I think that it is pretty consistently supported by different dbs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that reference should be valid in general, my point is that the generated SQL is invalid according to that reference. Try running the create schema statements for the first model to set it up and then running the migration statements, I'm pretty sure they will fail

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, also the change was referring to just the line I commented on, for reference I would expect the full block to be:

[ '''
	ALTER TABLE "pilot-can fly-plane"
	RENAME TO "plane-can be flown by-pilot";
	ALTER TABLE "plane-can be flown by-pilot"
	RENAME COLUMN "pilot" TO "can be flown by-pilot";
	ALTER TABLE "plane-can be flown by-pilot"
	RENAME COLUMN "can fly-plane" TO "plane";
''' ]

or

[ '''
	ALTER TABLE "pilot-can fly-plane"
	RENAME TO "plane-can be flown by-pilot";
''', '''
	ALTER TABLE "plane-can be flown by-pilot"
	RENAME COLUMN "pilot" TO "can be flown by-pilot";
''', '''
	ALTER TABLE "plane-can be flown by-pilot"
	RENAME COLUMN "can fly-plane" TO "plane";
''' ]

export const postgres = generateExport(Engines.postgres, true)
export const mysql = generateExport(Engines.mysql, true)
export const websql = generateExport(Engines.websql, false)
export const websql = generateExport(Engines.websql, false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put the newline back please

}

const compileSchema = (abstractSqlModel: AbstractSqlModel, engine: Engines, ifNotExists: boolean): SqlModel => {
let { hasDependants, schemaDependencyMap } = mkSchemaDependencyMap(abstractSqlModel.tables, engine, ifNotExists)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer const here

fields?: {}[]
}

type ReduceFn<T> = (accumulator: any, element: T) => any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer this to be better typed, my guess is you can do that for your purposes as

type ReduceFn<T> = (accumulator: T[], element: T) => T[]

[dependant: string]: true
}

export interface schemaDependencyMap {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use CamelCase for these interfaces please, to distinguish them from vars

}
}

const insFn: ReduceFn<AbstractSqlTable> = (acc, table ) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a stray space in here

// the argument or undefined if no such element is found.
const genDiff = <T>(insFn: ReduceFn<T>, delFn: ReduceFn<T>, modFn: ReduceFn<Pair<T>>, matchFn: MatchFn<T>, src: Array<T>, dst: Array<T>) => {
let split = genSplit(src, dst, matchFn)
return _.flatten(_.reduce(split.ins, insFn,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all this code could be made much nice with either

_.flatMap(split.mod, modFn)
.concat(_.flatMap(split.del, delFn))
.concat(_.flatMap(split.ins, insFn))

and then return [] to include nothing, and return [a, b, c] to return multiple. It avoids all the need for mutation and overall should end up much nicer imo. The alternative if you only ever need to return 0 or 1 results (never > 1) is:

_.map(split.mod, modFn)
.concat(_.map(split.del, delFn))
.concat(_.map(split.ins, insFn))
.reject(_.isNil)

and return for nothing, return a for something

@resin-io-modules-versionbot
Copy link
Contributor

@nazrhom, status checks have failed for this PR. Please make appropriate changes and recommit.

@nazrhom nazrhom requested a review from Page- October 24, 2017 16:01
@nazrhom nazrhom requested review from Page- and removed request for Page- October 26, 2017 09:57
@resin-io-modules-versionbot
Copy link
Contributor

@nazrhom, status checks have failed for this PR. Please make appropriate changes and recommit.

fields?: {}[]
}

type ReduceFn<T> = (element: T) => string | undefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect a reducing function to take two arguments, one for the current item and another for the accumulated result. Either this isn't a reducer, therefore needs a new name, or there's a bug :)

const compileSchema = (abstractSqlModel: AbstractSqlModel, engine: Engines, ifNotExists: boolean): SqlModel => {
let ifNotExistsStr: string
const mkSchemaDependencyMap = (tables: ResourceMap<AbstractSqlTable>, engine: Engines, ifNotExists: boolean) => {
let ifNotExistsStr: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a space and then a tab here.

}
})

return { hasDependants: hasDependants, schemaDependencyMap: schemaDependencyMap }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keys can be omitted.

return acc
}
}
, { ins: dst, del: src, mod: mod } )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a reason for using shortened names for identifiers -- what's wrong with inserted, deleted, modified, and generateSplit?

// insertion, deletion or modification.
// All these functions are treated as reducers, their first argument
// will be an accumulator of all the changes up to that point and
// they are expected to add their changes to it and return it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a reducer then. This is a sort of a mapping function that's also allowed to have side-effects? Seeing how you define these functions in diffFields and diffSchemas later on, how you use map in this function's body and this comment, this is all needlessly confusing.

Copy link
Contributor Author

@nazrhom nazrhom Oct 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed them in the last commit, given the comments Page did, they used to be reducers but aren't anymore and some comments/names where lagging behind. I removed every reference to reducers to make it clear. They are just mapping functions which are not expected to have side-effects, but just map to the corresponding sql statement.

return
}
if (_.isEqual(_.omit(src, ['fieldName', 'references']), _.omit(dst, ['fieldName', 'references']))) {
return 'RENAME COLUMN "' + src.fieldName + '" TO "' + dst.fieldName + '";\n\t'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these SQL statements be backend-specific (i.e. different for WebSQL, PostgreSQL, etc)? If Pine supported SQLite for example this wouldn't work, and makes me suspicious WebSQL might also have different syntax or no support even.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I assumed postgre for now, I think that most of these are pretty standard across SQL dialects but Ill check each statement.

let verb = dstTable.name.split('-').slice(1, -1).join(' ')
return !_.isUndefined(relations[verb])
})
} else return relations
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need for else, you can simply return relations from outside the if. Same below.

RENAME TO "plane-can be flown by-pilot";
ALTER TABLE "plane-can be flown by-pilot"
RENAME COLUMN "pilot" TO "can be flown by-pilot";
RENAME COLUMN "can fly-plane" TO "plane";\n\t
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still has the issue from #13 (comment)

Oh, also the change was referring to just the line I commented on, for reference I would expect the full block to be:

[ '''
	ALTER TABLE "pilot-can fly-plane"
	RENAME TO "plane-can be flown by-pilot";
	ALTER TABLE "plane-can be flown by-pilot"
	RENAME COLUMN "pilot" TO "can be flown by-pilot";
	ALTER TABLE "plane-can be flown by-pilot"
	RENAME COLUMN "can fly-plane" TO "plane";
''' ]

or

[ '''
	ALTER TABLE "pilot-can fly-plane"
	RENAME TO "plane-can be flown by-pilot";
''', '''
	ALTER TABLE "plane-can be flown by-pilot"
	RENAME COLUMN "pilot" TO "can be flown by-pilot";
''', '''
	ALTER TABLE "plane-can be flown by-pilot"
	RENAME COLUMN "can fly-plane" TO "plane";
''' ]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh I kept misreading, I thought that renames on columns where in the list of actions. I will change that everywhere so that each rename has its own alter table

@resin-io-modules-versionbot
Copy link
Contributor

@nazrhom, status checks have failed for this PR. Please make appropriate changes and recommit.

expectation(result)

runMigration = (describe, src, dst, expectation) ->
it 'Migration: \n\nVocabulary: src\n' + src + '\n\nVocabulary: dst\n' + dst, ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you use

describe 'Migration', ->
	it 'Vocabulary: src\n' + src + '\n\nVocabulary: dst\n' + dst, ->

it might come out in the results a little nicer

NestedIndent: function(indent) {
var $elf = this, _fromIdx = this.input.idx;
return indent + " ";
return indent + "\t";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is changed here but I don't see any changes to AbstractSQLRules2SQL.ometa?

Copy link
Contributor Author

@nazrhom nazrhom May 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this, my AbstractSQLRules2SQL.ometa is the same as the one appearing on master, I think this is caused by some other commit that was merged without including the changes to AbstractSQLRules.js?

const extractMappings = (resource:string):[string, string] => {
const resourceParts = resource.split('-')
const subject = resourceParts[0]
const rest = resourceParts.slice(1).join('-')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can make this nicer with

const [ subject, ...rest ] = resource.split('-')
return [ subject, rest.join('-') ]

const dstSDM = mkSchemaDependencyMap(dst.tables, engine, ifNotExists).schemaDependencyMap

const matchFn:MatchFn<AbstractSqlTable> = (tables, srcTable) => {
let match = _.find(tables, { name: srcTable.name })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be const


const matchFn:MatchFn<AbstractSqlTable> = (tables, srcTable) => {
let match = _.find(tables, { name: srcTable.name })
if (_.isUndefined(match)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be nicer as

if (match != null) {
	return match
}


const matchFn: MatchFn<AbstractSqlField> = (fieldArray, field) => {
let match = _.find(fieldArray, { fieldName: field.fieldName })
if (_.isUndefined(match)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you shortcircuit with

if (match != null) {
	return match
}

}

const matchFn: MatchFn<AbstractSqlField> = (fieldArray, field) => {
let match = _.find(fieldArray, { fieldName: field.fieldName })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be const

.concat(_.map(split.deleted, delFn))
.concat(_.map(split.inserted, insFn))

return _.reject(diff, _.isUndefined)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer _.isNil to catch both null and undefined

}

const generateSplit = <T>(src:Array<T>, dst: Array<T>, matchFn: MatchFn<T>): Split<T> => {
let modified: Array<Pair<T>> = []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be const

let modified: Array<Pair<T>> = []
return _.reduce(src, (acc, value) => {
const match = matchFn(acc.inserted, value)
if (_.isUndefined(match)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be match == null

Giovanni Garufi added 3 commits July 3, 2018 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants