From 39999ae5fc758241e37597275ad189c647996371 Mon Sep 17 00:00:00 2001 From: itsumura-h <39766805+itsumura-h@users.noreply.github.com> Date: Sat, 10 Jul 2021 15:05:02 +0900 Subject: [PATCH] Feature/alter table add foreign key (#152) * sqlite alter table add foreign key * postgre alter table add foreign key * mysql alter table add foreign key * sqlite alter table delete foreign * mysql alter table delete foreign * fix mysql alter table delete foreign * postgre alter table delete foreign * test seccess * update doc * fix run test * success test * success test * fix regex --- allographer.nimble | 2 +- documents/schema_builder.md | 14 +- src/allographer/schema_builder/alter.nim | 10 +- .../schema_builder/alters/mysql_alter.nim | 35 ++- .../schema_builder/alters/postgres_alter.nim | 36 ++- .../schema_builder/alters/sqlite_alter.nim | 25 +- src/allographer/schema_builder/column.nim | 239 +++++++++--------- .../generators/mysql_generators.nim | 31 ++- .../generators/postgres_generators.nim | 37 ++- .../generators/sqlite_generators.nim | 7 +- .../schema_builder/migrates/mysql_migrate.nim | 36 +++ .../migrates/postgres_migrate.nim | 49 +++- .../migrates/sqlite_migrate.nim | 18 ++ src/allographer/utils.nim | 8 +- tests/test_alter_table.nim | 23 +- tests/test_query.nim | 1 - tests/test_schema_builder.nim | 193 +++++++------- 17 files changed, 501 insertions(+), 263 deletions(-) diff --git a/allographer.nimble b/allographer.nimble index 958faa2d..53b32801 100644 --- a/allographer.nimble +++ b/allographer.nimble @@ -1,6 +1,6 @@ # Package -version = "0.17.5" +version = "0.18.0" author = "Hidenobu Itsumura @dumblepytech1 as 'medy'" description = "A Nim query builder library inspired by Laravel/PHP and Orator/Python" license = "MIT" diff --git a/documents/schema_builder.md b/documents/schema_builder.md index d61b6cb5..99f4da51 100644 --- a/documents/schema_builder.md +++ b/documents/schema_builder.md @@ -49,9 +49,14 @@ If you set `reset=true` in args of `Table().create`, `DROP TABLE` and `CREATE TA ### add column ```nim alter( - table("users", - add().string("email").unique().default("") - ) + table("auth", [ + add.increments("id"), + add.string("name"), + ]), + table("users",[ + add().string("email").unique().default(""), + add().foreign("auth_id").reference("id").on("auth").onDelete(SET_NULL) + ]) ) ``` `>> ALTER TABLE "users" ADD COLUMN 'email' UNIQUE DEFAULT '' CHECK (length('email') <= 255)` @@ -75,7 +80,8 @@ alter( ```nim alter( table("users", - delete("name") + delete().column("name") + delete().foreign("auth_id") ) ) ``` diff --git a/src/allographer/schema_builder/alter.nim b/src/allographer/schema_builder/alter.nim index 329ef56c..e303047c 100644 --- a/src/allographer/schema_builder/alter.nim +++ b/src/allographer/schema_builder/alter.nim @@ -24,11 +24,15 @@ proc add*():Column = proc change*(name:string):Column = return Column(alterTyp:Change, previousName:name) -proc delete*(name:string):Column = - return Column(alterTyp:Delete, previousName:name) - proc rename*(alterFrom, alterTo:string):Table = return Table(name:alterFrom, alterTo:alterTo, typ:Rename) proc drop*(name:string):Table = return Table(name:name, typ:Drop) + +proc delete*():Column = + return Column(alterTyp: Delete) + +proc column*(self:Column, name:string):Column = + self.name = name + return self diff --git a/src/allographer/schema_builder/alters/mysql_alter.nim b/src/allographer/schema_builder/alters/mysql_alter.nim index 4f5f4220..e19e14db 100644 --- a/src/allographer/schema_builder/alters/mysql_alter.nim +++ b/src/allographer/schema_builder/alters/mysql_alter.nim @@ -6,26 +6,28 @@ import ../../utils import ../../connection proc add(column:Column, table:string) = - let columnString = generateColumnString(column) - let query = &"ALTER TABLE {table} ADD {columnString}" - logger(query) + # let columnString = generateColumnString(column) + # let query = &"ALTER TABLE {table} ADD {columnString}" + let querySeq = generateAlterAddQueries(column, table) block: let db = db() defer: db.close() try: - db.exec(sql query) + for query in querySeq: + logger(query) + db.exec(sql query) except: let err = getCurrentExceptionMsg() echoErrorMsg(err) echoWarningMsg(&"Safety skip alter table '{table}'") -proc getColumns(table:string, previousName:string):string = +proc getColumns(table:string, name:string):string = let db = db() defer: db.close() var query = &"SHOW COLUMNS FROM {table}" var columns:string for i, row in db.getAllRows(sql query): - if row[0] != previousName: + if row[0] != name: if i > 0: columns.add(", ") columns.add(row[1]) return columns @@ -42,17 +44,29 @@ proc change(column:Column, table:string) = let err = getCurrentExceptionMsg() echoErrorMsg(err) -proc delete(column:Column, table:string) = +proc deleteColumn(table:string, column:Column) = let db = db() defer: db.close() try: - let query = &"ALTER TABLE {table} DROP `{column.previousName}`" + let query = generateAlterDeleteQuery(table, column) logger(query) db.exec(sql query) except: let err = getCurrentExceptionMsg() echoErrorMsg(err) +proc deleteForeign(table:string, column:Column) = + let querySeq = generateAlterDeleteForeignQueries(table, column) + let db = db() + defer: db.close() + try: + for query in querySeq: + logger(query) + db.exec(sql query) + except: + let err = getCurrentExceptionMsg() + echoErrorMsg(err) + proc rename(tableFrom, tableTo:string) = let db = db() defer: db.close() @@ -84,7 +98,10 @@ proc exec*(table:Table) = of Change: change(column, table.name) of Delete: - delete(column, table.name) + if column.typ == rdbForeign: + deleteForeign(table.name, column) + else: + deleteColumn(table.name, column) elif table.typ == Rename: rename(table.name, table.alterTo) elif table.typ == Drop: diff --git a/src/allographer/schema_builder/alters/postgres_alter.nim b/src/allographer/schema_builder/alters/postgres_alter.nim index 4c07788f..eccb24e4 100644 --- a/src/allographer/schema_builder/alters/postgres_alter.nim +++ b/src/allographer/schema_builder/alters/postgres_alter.nim @@ -6,14 +6,14 @@ import ../../utils import ../../connection proc add(column:Column, table:string) = - let columnString = generateColumnString(column) - let query = &"ALTER TABLE {table} ADD {columnString}" - logger(query) + let querySeq = migrateAlter(table, column) block: let db = db() defer: db.close() try: - db.exec(sql query) + for query in querySeq: + logger(query) + db.exec(sql query) except: let err = getCurrentExceptionMsg() echoErrorMsg(err) @@ -79,6 +79,29 @@ proc delete(column:Column, table:string) = let err = getCurrentExceptionMsg() echoErrorMsg(err) +proc deleteColumn(table:string, column:Column) = + let db = db() + defer: db.close() + try: + let query = generateAlterDeleteQuery(table, column) + logger(query) + db.exec(sql query) + except: + let err = getCurrentExceptionMsg() + echoErrorMsg(err) + +proc deleteForeign(table:string, column:Column) = + let querySeq = generateAlterDeleteForeignQueries(table, column) + let db = db() + defer: db.close() + try: + for query in querySeq: + logger(query) + db.exec(sql query) + except: + let err = getCurrentExceptionMsg() + echoErrorMsg(err) + proc rename(tableFrom, tableTo:string) = let db = db() defer: db.close() @@ -110,7 +133,10 @@ proc exec*(table:Table) = of Change: change(column, table.name) of Delete: - delete(column, table.name) + if column.typ == rdbForeign: + deleteForeign(table.name, column) + else: + deleteColumn(table.name, column) elif table.typ == Rename: rename(table.name, table.alterTo) elif table.typ == Drop: diff --git a/src/allographer/schema_builder/alters/sqlite_alter.nim b/src/allographer/schema_builder/alters/sqlite_alter.nim index 1e93e7d3..cbf14194 100644 --- a/src/allographer/schema_builder/alters/sqlite_alter.nim +++ b/src/allographer/schema_builder/alters/sqlite_alter.nim @@ -6,8 +6,7 @@ import ../../utils import ../../connection proc add(column:Column, table:string) = - let columnString = generateColumnString(column) - let query = &"ALTER TABLE \"{table}\" ADD COLUMN {columnString}" + let query = migrateAlter(column, table) logger(query) block: let db = db() @@ -69,7 +68,7 @@ proc getColumns(table:string, previousName:string):string = columns.add(row[1]) return columns -proc delete(column:Column, table:string) = +proc deleteColumn(column:Column, table:string) = ## rename existing table as tmp ## ## create new table with existing table name @@ -79,23 +78,32 @@ proc delete(column:Column, table:string) = ## delete tmp table let db = db() defer: db.close() + try: + # delete tmp table + let query = &"DROP TABLE alter_table_tmp" + logger(query) + db.exec(sql query) + except: + let err = getCurrentExceptionMsg() + echoErrorMsg(err) + try: # rename existing table as tmp - var query = &"ALTER TABLE {table} RENAME TO alter_table_tmp" + var query = &"ALTER TABLE \"{table}\" RENAME TO 'alter_table_tmp'" logger(query) db.exec(sql query) # create new table with existing table name let tableDifinitionSql = &"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'alter_table_tmp';" var schema = db.getValue(sql tableDifinitionSql) schema = replace(schema, re"\)$", ",)") - let columnRegex = &"'{column.previousName}'.*?," + let columnRegex = &"'{column.name}'.*?," query = replace(schema, re(columnRegex)) - query = replace(query, re",\)", ")") query = replace(query, re"alter_table_tmp", table) + query = replace(query, re",\s*\)", ")") logger(query) db.exec(sql query) # copy data from tmp table to new table - var columns = getColumns(table, column.previousName) + var columns = getColumns(table, column.name) query = &"INSERT INTO {table}({columns}) SELECT {columns} FROM alter_table_tmp" logger(query) db.exec(sql query) @@ -138,7 +146,8 @@ proc exec*(table:Table) = of Change: change(column, table.name) of Delete: - delete(column, table.name) + deleteColumn(column, table.name) + elif table.typ == Rename: rename(table.name, table.alterTo) elif table.typ == Drop: diff --git a/src/allographer/schema_builder/column.nim b/src/allographer/schema_builder/column.nim index 25498a54..e93ca3fa 100644 --- a/src/allographer/schema_builder/column.nim +++ b/src/allographer/schema_builder/column.nim @@ -114,165 +114,166 @@ proc unsigned*(c: Column): Column = # ============================================================================= # int # ============================================================================= -proc increments*(this:Column, name:string): Column = - this.name = name - this.typ = rdbIncrements - return this - -proc integer*(this:Column, name:string):Column = - this.name = name - this.typ = rdbInteger - return this - -proc smallInteger*(this:Column, name:string):Column = - this.name = name - this.typ = rdbSmallInteger - return this - -proc mediumInteger*(this:Column, name:string):Column = - this.name = name - this.typ = rdbMediumInteger - return this - -proc bigInteger*(this:Column, name:string):Column = - this.name = name - this.typ = rdbBigInteger - return this +proc increments*(self:Column, name:string): Column = + self.name = name + self.typ = rdbIncrements + return self + +proc integer*(self:Column, name:string):Column = + self.name = name + self.typ = rdbInteger + return self + +proc smallInteger*(self:Column, name:string):Column = + self.name = name + self.typ = rdbSmallInteger + return self + +proc mediumInteger*(self:Column, name:string):Column = + self.name = name + self.typ = rdbMediumInteger + return self + +proc bigInteger*(self:Column, name:string):Column = + self.name = name + self.typ = rdbBigInteger + return self # ============================================================================= # float # ============================================================================= -proc decimal*(this:Column, name:string, maximum:int, digit:int): Column = - this.name = name - this.typ = rdbDecimal - this.info = %*{ +proc decimal*(self:Column, name:string, maximum:int, digit:int): Column = + self.name = name + self.typ = rdbDecimal + self.info = %*{ "maximum": maximum, "digit": digit } - return this + return self -proc double*(this:Column, name:string, maximum:int, digit:int):Column = - this.name = name - this.typ = rdbDouble - this.info = %*{ +proc double*(self:Column, name:string, maximum:int, digit:int):Column = + self.name = name + self.typ = rdbDouble + self.info = %*{ "maximum": maximum, "digit": digit } - return this + return self -proc float*(this:Column, name:string):Column = - this.name = name - this.typ = rdbFloat - return this +proc float*(self:Column, name:string):Column = + self.name = name + self.typ = rdbFloat + return self # ============================================================================= # char # ============================================================================= -proc char*(this:Column, name:string, maxLength:int): Column = - this.name = name - this.typ = rdbChar - this.info = %*{ +proc char*(self:Column, name:string, maxLength:int): Column = + self.name = name + self.typ = rdbChar + self.info = %*{ "maxLength": maxLength } - return this + return self -proc string*(this:Column, name:string, length=255):Column = - this.name = name - this.typ = rdbString - this.info = %*{"maxLength": length} - return this +proc string*(self:Column, name:string, length=255):Column = + self.name = name + self.typ = rdbString + self.info = %*{"maxLength": length} + return self -proc text*(this:Column, name:string):Column = - this.name = name - this.typ = rdbText - return this +proc text*(self:Column, name:string):Column = + self.name = name + self.typ = rdbText + return self -proc mediumText*(this:Column, name:string):Column = - this.name = name - this.typ = rdbMediumText - return this +proc mediumText*(self:Column, name:string):Column = + self.name = name + self.typ = rdbMediumText + return self -proc longText*(this:Column, name:string):Column = - this.name = name - this.typ = rdbLongText - return this +proc longText*(self:Column, name:string):Column = + self.name = name + self.typ = rdbLongText + return self # ============================================================================= # date # ============================================================================= -proc date*(this:Column, name:string):Column = - this.name = name - this.typ = rdbDate - return this - -proc datetime*(this:Column, name:string):Column = - this.name = name - this.typ = rdbDatetime - return this - -proc time*(this:Column, name:string):Column = - this.name = name - this.typ = rdbTime - return this - -proc timestamp*(this:Column, name:string):Column = - this.name = name - this.typ = rdbTimestamp - return this - -proc timestamps*(this:Column):Column = - this.typ = rdbTimestamps - return this - -proc softDelete*(this:Column):Column = - this.typ = rdbSoftDelete - return this +proc date*(self:Column, name:string):Column = + self.name = name + self.typ = rdbDate + return self + +proc datetime*(self:Column, name:string):Column = + self.name = name + self.typ = rdbDatetime + return self + +proc time*(self:Column, name:string):Column = + self.name = name + self.typ = rdbTime + return self + +proc timestamp*(self:Column, name:string):Column = + self.name = name + self.typ = rdbTimestamp + return self + +proc timestamps*(self:Column):Column = + self.typ = rdbTimestamps + return self + +proc softDelete*(self:Column):Column = + self.typ = rdbSoftDelete + return self # ============================================================================= # others # ============================================================================= -proc binary*(this:Column, name:string): Column = - this.name = name - this.typ = rdbBinary - return this +proc binary*(self:Column, name:string): Column = + self.name = name + self.typ = rdbBinary + return self # ============================================================================= -proc boolean*(this:Column, name:string): Column = - this.name = name - this.typ = rdbBoolean - return this - -proc enumField*(this:Column, name:string, options:openArray[string]):Column = - this.name = name - this.typ = rdbEnumField - this.info = %*{ +proc boolean*(self:Column, name:string): Column = + self.name = name + self.typ = rdbBoolean + return self + +proc enumField*(self:Column, name:string, options:openArray[string]):Column = + self.name = name + self.typ = rdbEnumField + self.info = %*{ "options": options } - return this + return self -proc json*(this:Column, name:string):Column = - this.name = name - this.typ = rdbJson - return this +proc json*(self:Column, name:string):Column = + self.name = name + self.typ = rdbJson + return self # ============================================================================= # Foreign # ============================================================================= -proc foreign*(this:Column, name:string):Column = - this.name = name - this.typ = rdbForeign - return this - -proc reference*(this:Column, column:string):Column = - this.info = %*{ +proc foreign*(self:Column, name:string):Column = + self.name = name + self.previousName = name + self.typ = rdbForeign + return self + +proc reference*(self:Column, column:string):Column = + self.info = %*{ "column": column } - return this + return self -proc on*(this:Column, table:string):Column = - this.info["table"] = %*table - return this +proc on*(self:Column, table:string):Column = + self.info["table"] = %*table + return self -proc onDelete*(this:Column, kind:ForeignOnDelete):Column = - this.foreignOnDelete = kind - return this +proc onDelete*(self:Column, kind:ForeignOnDelete):Column = + self.foreignOnDelete = kind + return self diff --git a/src/allographer/schema_builder/generators/mysql_generators.nim b/src/allographer/schema_builder/generators/mysql_generators.nim index 0ead497c..eb007fec 100644 --- a/src/allographer/schema_builder/generators/mysql_generators.nim +++ b/src/allographer/schema_builder/generators/mysql_generators.nim @@ -377,8 +377,35 @@ proc foreignGenerator*(name:string, table:string, column:string, elif foreignOnDelete == NO_ACTION: onDeleteString = "NO ACTION" - result = &", FOREIGN KEY(`{name}`) REFERENCES {table}({column})" - result.add(&" ON DELETE {onDeleteString}") + return &", FOREIGN KEY(`{name}`) REFERENCES {table}({column}) ON DELETE {onDeleteString}" + +proc alterAddForeignGenerator*(table, column, refTable, refColumn:string, + foreignOnDelete:ForeignOnDelete):string = + var onDeleteString = "RESTRICT" + if foreignOnDelete == CASCADE: + onDeleteString = "CASCADE" + elif foreignOnDelete == SET_NULL: + onDeleteString = "SET NULL" + elif foreignOnDelete == NO_ACTION: + onDeleteString = "NO ACTION" + + var constraintName = &"{table}_{column}" + wrapUpper(constraintName) + var refTable = refTable + wrapUpper(refTable) + return &"CONSTRAINT {constraintName} FOREIGN KEY (`{column}`) REFERENCES {refTable} ({refColumn}) ON DELETE {onDeleteString}" + +proc alterDeleteGenerator*(table:string, column:string):string = + var table = table + wrapUpper(table) + return &"ALTER TABLE {table} DROP `{column}`" + +proc alterDeleteForeignGenerator*(table, column:string):string = + var constraintName = &"{table}_{column}" + wrapUpper(constraintName) + var table = table + wrapUpper(table) + return &"ALTER TABLE {table} DROP FOREIGN KEY {constraintName}" proc indexGenerate*(table, column:string):string = var table = table diff --git a/src/allographer/schema_builder/generators/postgres_generators.nim b/src/allographer/schema_builder/generators/postgres_generators.nim index bb16932b..9b0b87f5 100644 --- a/src/allographer/schema_builder/generators/postgres_generators.nim +++ b/src/allographer/schema_builder/generators/postgres_generators.nim @@ -391,7 +391,7 @@ proc foreignColumnGenerator*(name:string, isDefault:bool, default:int):string = if isDefault: result.add(&" DEFAULT {default}") -proc foreignGenerator*(name:string, tableName:string, column:string, +proc foreignGenerator*(table, column, refTable, refColumn:string, foreignOnDelete:ForeignOnDelete):string = var onDeleteString = "RESTRICT" if foreignOnDelete == CASCADE: @@ -401,10 +401,37 @@ proc foreignGenerator*(name:string, tableName:string, column:string, elif foreignOnDelete == NO_ACTION: onDeleteString = "NO ACTION" - var tableName = tableName - wrapUpper(tableName) - result = &", FOREIGN KEY(\"{name}\") REFERENCES {tableName}({column})" - result.add(&" ON DELETE {onDeleteString}") + var refTable = refTable + wrapUpper(refTable) + return &", FOREIGN KEY(\"{column}\") REFERENCES {refTable}({refColumn}) ON DELETE {onDeleteString}" + +proc alterAddForeignGenerator*(table, column, refTable, refColumn:string, + foreignOnDelete:ForeignOnDelete):string = + var onDeleteString = "RESTRICT" + if foreignOnDelete == CASCADE: + onDeleteString = "CASCADE" + elif foreignOnDelete == SET_NULL: + onDeleteString = "SET NULL" + elif foreignOnDelete == NO_ACTION: + onDeleteString = "NO ACTION" + + var constraintName = &"{table}_{column}" + wrapUpper(constraintName) + var refTable = refTable + wrapUpper(refTable) + return &"CONSTRAINT {constraintName} FOREIGN KEY (\"{column}\") REFERENCES {refTable} ({refColumn}) ON DELETE {onDeleteString}" + +proc alterDeleteGenerator*(table:string, column:string):string = + var table = table + wrapUpper(table) + return &"ALTER TABLE {table} DROP {column}" + +proc alterDeleteForeignGenerator*(table, column:string):string = + var constraintName = &"{table}_{column}" + wrapUpper(constraintName) + var table = table + wrapUpper(table) + return &"ALTER TABLE {table} DROP CONSTRAINT {constraintName}" proc indexGenerate*(table, column:string):string = var table = table diff --git a/src/allographer/schema_builder/generators/sqlite_generators.nim b/src/allographer/schema_builder/generators/sqlite_generators.nim index 4b845f4a..e7bb79c4 100644 --- a/src/allographer/schema_builder/generators/sqlite_generators.nim +++ b/src/allographer/schema_builder/generators/sqlite_generators.nim @@ -286,8 +286,11 @@ proc foreignGenerator*(name:string, table:string, column:string, elif foreignOnDelete == NO_ACTION: onDeleteString = "NO ACTION" - result = &", FOREIGN KEY('{name}') REFERENCES {table}({column})" - result.add(&" ON DELETE {onDeleteString}") + return &", FOREIGN KEY('{name}') REFERENCES {table}({column}) ON DELETE {onDeleteString}" + +proc alterAddForeignGenerator*(table:string, column:string):string = + return &"REFERENCES {table}({column})" + proc indexGenerate*(table, column:string):string = var table = table diff --git a/src/allographer/schema_builder/migrates/mysql_migrate.nim b/src/allographer/schema_builder/migrates/mysql_migrate.nim index 57f707ba..8934f054 100644 --- a/src/allographer/schema_builder/migrates/mysql_migrate.nim +++ b/src/allographer/schema_builder/migrates/mysql_migrate.nim @@ -264,6 +264,16 @@ proc generateForeignString(column:Column):string = column.foreignOnDelete ) +proc generateAlterForeignString(table:string, column:Column):string = + if column.typ == rdbForeign: + return alterAddForeignGenerator( + table, + column.name, + column.info["table"].getStr(), + column.info["column"].getStr(), + column.foreignOnDelete + ) + proc migrate*(this:Table):string = var columnString = "" var foreignString = "" @@ -280,5 +290,31 @@ proc migrate*(this:Table):string = wrapUpper(tableName) return &"CREATE TABLE {tableName} ({columnString}{foreignString})" +proc generateAlterAddQueries*(column:Column, table:string):seq[string] = + let columnString = generateColumnString(column) + let foreignString = generateAlterForeignString(table, column) + + result = @[ + &"ALTER TABLE `{table}` ADD COLUMN {columnString}" + ] + + if foreignString.len > 0: + result.add( &"ALTER TABLE `{table}` ADD {foreignString}" ) + +proc generateAlterDeleteQuery*(table:string, column:Column):string = + return alterDeleteGenerator(table, column.name) + +proc generateAlterDeleteForeignQueries*(table:string, column:Column):seq[string] = + return @[ + alterDeleteForeignGenerator( + table, + column.name, + ), + alterDeleteGenerator( + table, + column.name + ) + ] + proc createIndex*(table, column:string):string = return indexGenerate(table, column) diff --git a/src/allographer/schema_builder/migrates/postgres_migrate.nim b/src/allographer/schema_builder/migrates/postgres_migrate.nim index b5a7d8fb..25bb7ee0 100644 --- a/src/allographer/schema_builder/migrates/postgres_migrate.nim +++ b/src/allographer/schema_builder/migrates/postgres_migrate.nim @@ -275,30 +275,67 @@ proc generateColumnString*(column:Column, tableName=""):string = ) return columnString -proc generateForeignString(column:Column):string = +proc generateForeignString(table:string, column:Column):string = if column.typ == rdbForeign: return foreignGenerator( + table, column.name, column.info["table"].getStr(), column.info["column"].getStr(), column.foreignOnDelete ) -proc migrate*(this:Table):string = +proc generateAlterForeignString(table:string, column:Column):string = + if column.typ == rdbForeign: + return alterAddForeignGenerator( + table, + column.name, + column.info["table"].getStr(), + column.info["column"].getStr(), + column.foreignOnDelete + ) + +proc migrate*(self:Table):string = var columnString = "" var foreignString = "" - for i, column in this.columns: + for i, column in self.columns: if i > 0: columnString.add(", ") columnString.add( - generateColumnString(column, this.name) + generateColumnString(column, self.name) ) foreignString.add( - generateForeignString(column) + generateForeignString(self.name, column) ) - var tableName = this.name + var tableName = self.name wrapUpper(tableName) return &"CREATE TABLE {tableName} ({columnString}{foreignString})" +proc migrateAlter*(table:string, column:Column):seq[string] = + let columnString = generateColumnString(column, table) + let foreignString = generateAlterForeignString(table, column) + + result = @[ + &"ALTER TABLE \"{table}\" ADD COLUMN {columnString}" + ] + + if foreignString.len > 0: + result.add( &"ALTER TABLE \"{table}\" ADD {foreignString}" ) + +proc generateAlterDeleteQuery*(table:string, column:Column):string = + return alterDeleteGenerator(table, column.name) + +proc generateAlterDeleteForeignQueries*(table:string, column:Column):seq[string] = + return @[ + alterDeleteForeignGenerator( + table, + column.name, + ), + alterDeleteGenerator( + table, + column.name + ) + ] + proc createIndex*(table, column:string):string = return indexGenerate(table, column) diff --git a/src/allographer/schema_builder/migrates/sqlite_migrate.nim b/src/allographer/schema_builder/migrates/sqlite_migrate.nim index 5aeb8de3..a57bb46c 100644 --- a/src/allographer/schema_builder/migrates/sqlite_migrate.nim +++ b/src/allographer/schema_builder/migrates/sqlite_migrate.nim @@ -263,6 +263,13 @@ proc generateForeignString(column:Column):string = column.foreignOnDelete ) +proc generateAlterForeignString(column:Column):string = + if column.typ == rdbForeign: + return alterAddForeignGenerator( + column.info["table"].getStr(), + column.info["column"].getStr(), + ) + proc migrate*(this:Table):string = var columnString = "" var foreignString = "" @@ -271,11 +278,22 @@ proc migrate*(this:Table):string = columnString.add( generateColumnString(column) ) + if foreignString.len > 0: foreignString.add(", ") foreignString.add( generateForeignString(column) ) return &"CREATE TABLE \"{this.name}\" ({columnString}{foreignString})" +proc migrateAlter*(column:Column, table:string):string = + let columnString = generateColumnString(column) + let foreignString = generateAlterForeignString(column) + + if foreignString.len > 0: + return &"ALTER TABLE \"{table}\" ADD COLUMN {columnString} {foreignString}" + else: + return &"ALTER TABLE \"{table}\" ADD COLUMN {columnString}" + + proc createIndex*(table, column:string):string = return indexGenerate(table, column) diff --git a/src/allographer/utils.nim b/src/allographer/utils.nim index 18a31072..4313f631 100644 --- a/src/allographer/utils.nim +++ b/src/allographer/utils.nim @@ -62,7 +62,13 @@ proc liteWrapUpper(input:var string) = input = &"\"{input}\"" proc myWrapUpper(input:var string) = - input = &"`{input}`" + var isUpper = false + for c in input: + if c.isUpperAscii(): + isUpper = true + break + if isUpper: + input = &"`{input}`" proc pgWrapUpper(input:var string) = var isUpper = false diff --git a/tests/test_alter_table.nim b/tests/test_alter_table.nim index 0fd5b647..70f6174c 100644 --- a/tests/test_alter_table.nim +++ b/tests/test_alter_table.nim @@ -4,6 +4,10 @@ import ../src/allographer/schema_builder import ../src/allographer/query_builder schema( + table("foreign_key_ref", [ + Column().increments("id"), + Column().string("name") + ], reset=true), table("table_alter", [ Column().increments("id"), Column().string("changed_column").unique().default(""), @@ -61,6 +65,23 @@ suite "alter table": .first.get["add_column"] .getStr == "test" + test "add foreign key": + alter( + table("table_alter", [ + delete().column("add_foreign_column"), + add().foreign("add_foreign_column").reference("id").on("foreign_key_ref").onDelete(SET_NULL), + ]) + ) + check rdb().table("table_alter").select("add_foreign_column").first.isSome == true + + alter( + table("table_alter", [ + delete().foreign("add_foreign_column"), + ]) + ) + check rdb().table("table_alter").select("add_foreign_column").first.isSome == false + + test "changed_column": echo rdb() .table("table_alter") @@ -104,7 +125,7 @@ suite "alter table": alter( table("table_alter", [ - delete("delete_column") + delete().column("delete_column") ]) ) diff --git a/tests/test_query.nim b/tests/test_query.nim index 70f2deea..aa4147a2 100644 --- a/tests/test_query.nim +++ b/tests/test_query.nim @@ -244,6 +244,5 @@ suite "select": test "raw query": let sql = "SELECT * FROM users WHERE id = ?" var res = rdb().raw(sql, "1").getRaw() - rdb().raw(sql, "1").exec() echo res check res[0]["name"].getStr == "user1" diff --git a/tests/test_schema_builder.nim b/tests/test_schema_builder.nim index 2b8e5bad..5cb7ad0b 100644 --- a/tests/test_schema_builder.nim +++ b/tests/test_schema_builder.nim @@ -1,130 +1,131 @@ -import unittest, json, times +import unittest, json, times, os import ../src/allographer/schema_builder import ../src/allographer/query_builder +let dbTyp = getEnv("DB_DRIVER", "sqlite") suite "Schema builder": test "test": schema([ table("sqlite", [ - Column().increments("increments"), - Column().integer("integer").unique().default(1).unsigned().index(), - Column().smallInteger("smallInteger").unique().default(1).unsigned().index(), - Column().mediumInteger("mediumInteger").unique().default(1).unsigned().index(), - Column().bigInteger("bigInteger").unique().default(1).unsigned().index(), - - Column().decimal("decimal", 5, 2).unique().default(1).unsigned().index(), - Column().double("double", 5, 2).unique().default(1).unsigned().index(), - Column().float("float").unique().default(1).unsigned().index(), - - Column().char("char", 100).unique().default("").unsigned().index(), - Column().string("string").unique().default("").unsigned().index(), - Column().text("text").unique().default("").unsigned().index(), - Column().mediumText("mediumText").unique().default("").unsigned().index(), - Column().longText("longText").unique().default("").unsigned().index(), - - Column().date("date").unique().default().unsigned().index(), - Column().datetime("datetime").unique().default().unsigned().index(), - Column().time("time").unique().default().unsigned().index(), - Column().timestamp("timestamp").unique().default().unsigned().index(), + Column().increments("increments_column"), + Column().integer("integer_column").unique().default(1).unsigned().index(), + Column().smallInteger("smallInteger_column").unique().default(1).unsigned().index(), + Column().mediumInteger("mediumInteger_column").unique().default(1).unsigned().index(), + Column().bigInteger("bigInteger_column").unique().default(1).unsigned().index(), + + Column().decimal("decimal_column", 5, 2).unique().default(1).unsigned().index(), + Column().double("double_column", 5, 2).unique().default(1).unsigned().index(), + Column().float("float_column").unique().default(1).unsigned().index(), + + Column().char("char_column", 100).unique().default("").unsigned().index(), + Column().string("string_column").unique().default("").unsigned().index(), + Column().text("text_column").unique().default("").unsigned().index(), + Column().mediumText("mediumText_column").unique().default("").unsigned().index(), + Column().longText("longText_column").unique().default("").unsigned().index(), + + Column().date("date_column").unique().default().unsigned().index(), + Column().datetime("datetime_column").unique().default().unsigned().index(), + Column().time("time_column").unique().default().unsigned().index(), + Column().timestamp("timestamp_column").unique().default().unsigned().index(), Column().timestamps(), Column().softDelete(), - Column().binary("binary").unique().default().unsigned().index(), - Column().boolean("boolean").unique().default().index(), - Column().enumField("enumField", ["a", "b"]).unique().default().index(), - Column().json("json").unique().default(%*{"key": "value"}).unsigned().index(), + Column().binary("binary_column").unique().default().unsigned().index(), + Column().boolean("boolean_column").unique().default().index(), + Column().enumField("enumField_column", ["a", "b"]).unique().default().index(), + Column().json("json_column").unique().default(%*{"key": "value"}).unsigned().index(), ], reset=true), # table("mysql", [ - # Column().increments("increments"), - # Column().integer("integer").unique().default(1).unsigned(), - # Column().smallInteger("smallInteger").unique().default(1).unsigned(), - # Column().mediumInteger("mediumInteger").unique().default(1).unsigned(), - # Column().bigInteger("bigInteger").unique().default(1).unsigned(), - - # Column().decimal("decimal", 5, 2).unique().default(1).unsigned(), - # Column().double("double", 5, 2).unique().default(1).unsigned(), - # Column().float("float").unique().default(1).unsigned(), - - # Column().char("char", 100).unique().default(""), - # Column().string("string").unique().default(""), - # Column().text("text"), - # Column().mediumText("mediumText"), - # Column().longText("longText"), - - # Column().date("date").unique().default(), - # Column().datetime("datetime").unique().default(), - # Column().time("time").unique().default(), - # Column().timestamp("timestamp").unique().default(), + # Column().increments("increments_column"), + # Column().integer("integer_column").unique().default(1).unsigned(), + # Column().smallInteger("smallInteger_column").unique().default(1).unsigned(), + # Column().mediumInteger("mediumInteger_column").unique().default(1).unsigned(), + # Column().bigInteger("bigInteger_column").unique().default(1).unsigned(), + + # Column().decimal("decimal_column", 5, 2).unique().default(1).unsigned(), + # Column().double("double_column", 5, 2).unique().default(1).unsigned(), + # Column().float("float_column").unique().default(1).unsigned(), + + # Column().char("char_column", 100).unique().default(""), + # Column().string("string_column").unique().default(""), + # Column().text("text_column"), + # Column().mediumText("mediumText_column"), + # Column().longText("longText_column"), + + # Column().date("date_column").unique().default(), + # Column().datetime("datetime_column").unique().default(), + # Column().time("time_column").unique().default(), + # Column().timestamp("timestamp_column").unique().default(), # Column().timestamps(), # Column().softDelete(), - # Column().binary("binary"), - # Column().boolean("boolean").unique().default(), - # Column().enumField("enumField", ["a", "b"]).unique().default("a"), - # Column().json("json"), + # Column().binary("binary_column"), + # Column().boolean("boolean_column").unique().default(), + # Column().enumField("enumField_column", ["a", "b"]).unique().default("a"), + # Column().json("json_column"), # ], reset=true), # table("postgres", [ - # Column().increments("increments"), - # Column().integer("integer").unique().default(1).unsigned(), - # Column().smallInteger("smallInteger").unique().default(1).unsigned(), - # Column().mediumInteger("mediumInteger").unique().default(1).unsigned(), - # Column().bigInteger("bigInteger").unique().default(1).unsigned(), - - # Column().decimal("decimal", 5, 2).unique().default(1).unsigned(), - # Column().double("double", 5, 2).unique().default(1).unsigned(), - # Column().float("float").unique().default(1).unsigned(), - - # Column().char("char", 100).unique().default(""), - # Column().string("string").unique().default(""), - # Column().text("text").unique().default(""), - # Column().mediumText("mediumText").unique().default(""), - # Column().longText("longText").unique().default(""), - - # Column().date("date").unique().default(), - # Column().datetime("datetime").unique().default(), - # Column().time("time").unique().default(), - # Column().timestamp("timestamp").unique().default(), + # Column().increments("increments_column"), + # Column().integer("integer_column").unique().default(1).unsigned(), + # Column().smallInteger("smallInteger_column").unique().default(1).unsigned(), + # Column().mediumInteger("mediumInteger_column").unique().default(1).unsigned(), + # Column().bigInteger("bigInteger_column").unique().default(1).unsigned(), + + # Column().decimal("decimal_column", 5, 2).unique().default(1).unsigned(), + # Column().double("double_column", 5, 2).unique().default(1).unsigned(), + # Column().float("float_column").unique().default(1).unsigned(), + + # Column().char("char_column", 100).unique().default(""), + # Column().string("string_column").unique().default(""), + # Column().text("text_column").unique().default(""), + # Column().mediumText("mediumText_column").unique().default(""), + # Column().longText("longText_column").unique().default(""), + + # Column().date("date_column").unique().default(), + # Column().datetime("datetime_column").unique().default(), + # Column().time("time_column").unique().default(), + # Column().timestamp("timestamp_column").unique().default(), # Column().timestamps(), # Column().softDelete(), - # Column().binary("binary").unique().default(), - # Column().boolean("boolean").unique().default(), - # Column().enumField("enumField", ["a", "b"]).unique().default(), - # Column().json("json").default(%*{"key": "value"}), + # Column().binary("binary_column").unique().default(), + # Column().boolean("boolean_column").unique().default(), + # Column().enumField("enumField_column", ["a", "b"]).unique().default(), + # Column().json("json_column").default(%*{"key": "value"}), # ], reset=true) ]) test "insert": try: - rdb().table("sqlite").insert(%*{ - "increments": 1, - "integer": 1, - "smallInteger": 1, - "mediumInteger": 1, - "bigInteger": 1, - "decimal": 111.11, - "double": 111.11, - "float": 111.11, - "char": "a", - "string": "a", - "text": "a", - "mediumText": "a", - "longText": "a", - "date": "2020-01-01".parse("yyyy-MM-dd").format("yyyy-MM-dd"), - "datetime": "2020-01-01".parse("yyyy-MM-dd").format("yyyy-MM-dd HH:MM:ss"), - "time": "2020-01-01".parse("yyyy-MM-dd").format("HH:MM:ss"), - "timestamp": "2020-01-01".parse("yyyy-MM-dd").format("yyyy-MM-dd HH:MM:ss"), - "binary": "a", - "boolean": true, - "enumField": "a", - "json": {"key": "value"} + rdb().table(dbTyp).insert(%*{ + "increments_column": 1, + "integer_column": 1, + "smallInteger_column": 1, + "mediumInteger_column": 1, + "bigInteger_column": 1, + "decimal_column": 111.11, + "double_column": 111.11, + "float_column": 111.11, + "char_column": "a", + "string_column": "a", + "text_column": "a", + "mediumText_column": "a", + "longText_column": "a", + "date_column": "2020-01-01".parse("yyyy-MM-dd").format("yyyy-MM-dd"), + "datetime_column": "2020-01-01".parse("yyyy-MM-dd").format("yyyy-MM-dd HH:MM:ss"), + "time_column": "2020-01-01".parse("yyyy-MM-dd").format("HH:MM:ss"), + "timestamp_column": "2020-01-01".parse("yyyy-MM-dd").format("yyyy-MM-dd HH:MM:ss"), + "binary_column": "a", + "boolean_column": true, + "enumField_column": "a", + "json_column": {"key": "value"} }) assert true alter( - drop("sqlite") + drop(dbTyp) ) except: echo getCurrentExceptionMsg()