ihp-ide-1.5.0: Test/IDE/SchemaDesigner/SchemaOperationsSpec.hs
module Test.IDE.SchemaDesigner.SchemaOperationsSpec where
import Test.Hspec
import IHP.Prelude
import IHP.Postgres.Types
import qualified IHP.IDE.SchemaDesigner.SchemaOperations as SchemaOperations
import qualified IHP.Postgres.Parser as Parser
import qualified Text.Megaparsec as Megaparsec
tests = do
describe "IHP.IDE.SchemaDesigner.SchemaOperations" do
let tableA = StatementCreateTable (table "a")
let tableB = StatementCreateTable (table "b")
let enumA = CreateEnumType { name = "enumA", values = [] }
let enumB = CreateEnumType { name = "enumB", values = [] }
let comment = Comment { content = "comment" }
describe "addEnum" do
it "should prepend the enum" do
let inputSchema = [tableA, tableB]
let expectedSchema = [enumA, tableA, tableB ]
(SchemaOperations.addEnum "enumA" inputSchema) `shouldBe` expectedSchema
it "should prepend the enum, but after existing enums" do
let inputSchema = [enumA, tableA]
let expectedSchema = [enumA, enumB, tableA]
(SchemaOperations.addEnum "enumB" inputSchema) `shouldBe` expectedSchema
it "should deal with the empty case" do
let inputSchema = []
let expectedSchema = [enumA]
(SchemaOperations.addEnum "enumA" inputSchema) `shouldBe` expectedSchema
it "should ignore comments" do
let inputSchema = [comment, enumA, tableA]
let expectedSchema = [comment, enumA, enumB, tableA]
(SchemaOperations.addEnum "enumB" inputSchema) `shouldBe` expectedSchema
describe "enableRowLevelSecurity" do
it "should enable row level security if not enabled yet" do
let inputSchema = [tableA]
let expectedSchema = [tableA, EnableRowLevelSecurity { tableName = "a"} ]
(SchemaOperations.enableRowLevelSecurity "a" inputSchema) `shouldBe` expectedSchema
it "should not do anything if already enabled" do
let inputSchema = [tableA, EnableRowLevelSecurity { tableName = "a"} ]
let expectedSchema = [tableA, EnableRowLevelSecurity { tableName = "a"} ]
(SchemaOperations.enableRowLevelSecurity "a" inputSchema) `shouldBe` expectedSchema
describe "disableRowLevelSecurity" do
it "should disable row level security if enabled" do
let inputSchema = [tableA, EnableRowLevelSecurity { tableName = "a"}]
let expectedSchema = [tableA]
(SchemaOperations.disableRowLevelSecurity "a" inputSchema) `shouldBe` expectedSchema
it "should not do anything if the row level security is not enabled" do
let inputSchema = [tableA]
let expectedSchema = [tableA]
(SchemaOperations.disableRowLevelSecurity "a" inputSchema) `shouldBe` expectedSchema
describe "disableRowLevelSecurityIfNoPolicies" do
it "should disable row level security if there's no policy" do
let inputSchema = [tableA, EnableRowLevelSecurity { tableName = "a"}]
let expectedSchema = [tableA]
(SchemaOperations.disableRowLevelSecurityIfNoPolicies "a" inputSchema) `shouldBe` expectedSchema
it "should not do anything if the row level security is not enabled" do
let inputSchema = [tableA]
(SchemaOperations.disableRowLevelSecurityIfNoPolicies "a" inputSchema) `shouldBe` inputSchema
it "should not do anything if there's a policy" do
let p = policy "p" "a"
let inputSchema = [tableA, EnableRowLevelSecurity { tableName = "a"}, p]
(SchemaOperations.disableRowLevelSecurityIfNoPolicies "a" inputSchema) `shouldBe` inputSchema
describe "deleteTable" do
it "delete a table with all it's indices, constraints, policies, enable RLS statements, triggers" do
let inputSchema = parseSqlStatements [trimming|
CREATE TABLE users ();
CREATE TABLE tasks ();
CREATE INDEX tasks_user_id_index ON tasks (user_id);
ALTER TABLE tasks ADD CONSTRAINT tasks_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
CREATE POLICY "Users can manage their tasks" ON tasks USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
CREATE TRIGGER update_tasks_updated_at BEFORE UPDATE ON tasks FOR EACH ROW EXECUTE FUNCTION set_updated_at_to_now();
|]
let outputSchema = parseSqlStatements [trimming|
CREATE TABLE users ();
|]
SchemaOperations.deleteTable "tasks" inputSchema `shouldBe` outputSchema
describe "suggestPolicy" do
it "should suggest a policy if a user_id column exists" do
let postsTable = StatementCreateTable (table "posts")
{ columns =
[ (col "user_id" PUUID) { notNull = True }
]
}
let schema = [postsTable]
let expectedPolicy = (policy "Users can manage their posts" "posts")
{ using = Just (EqExpression (VarExpression "user_id") (CallExpression "ihp_user_id" []))
, check = Just (EqExpression (VarExpression "user_id") (CallExpression "ihp_user_id" []))
}
SchemaOperations.suggestPolicy schema postsTable `shouldBe` expectedPolicy
it "should suggest an empty policy if no user_id column exists" do
let postsTable = StatementCreateTable (table "posts")
{ columns =
[ (col "title" PText) { notNull = True }
]
}
let schema = [postsTable]
let expectedPolicy = policy "" "posts"
SchemaOperations.suggestPolicy schema postsTable `shouldBe` expectedPolicy
it "should suggest a policy if it can find a one hop path to a user_id column" do
let tasksTable = StatementCreateTable (table "tasks")
{ columns =
[ (col "task_list_id" PUUID) { notNull = True }
]
}
let taskListsTable = StatementCreateTable (table "task_lists")
{ columns =
[ (col "user_id" PUUID) { notNull = True }
]
}
let schema =
[ tasksTable
, taskListsTable
, AddConstraint { tableName = "tasks", constraint = ForeignKeyConstraint { name = "tasks_ref_task_lists", columnName = "task_list_id", referenceTable = "task_lists", referenceColumn = Nothing, onDelete = Nothing }, deferrable = Nothing, deferrableType = Nothing }
]
let expectedPolicy = (policy "Users can manage the tasks if they can see the TaskList" "tasks")
{ using = Just (ExistsExpression (SelectExpression (Select {columns = [IntExpression 1], from = DotExpression (VarExpression "public") "task_lists", alias = Nothing, whereClause = EqExpression (DotExpression (VarExpression "task_lists") "id") (DotExpression (VarExpression "tasks") "task_list_id")})))
, check = Just (ExistsExpression (SelectExpression (Select {columns = [IntExpression 1], from = DotExpression (VarExpression "public") "task_lists", alias = Nothing, whereClause = EqExpression (DotExpression (VarExpression "task_lists") "id") (DotExpression (VarExpression "tasks") "task_list_id")})))
}
SchemaOperations.suggestPolicy schema tasksTable `shouldBe` expectedPolicy
describe "addColumn" do
it "should add an index if withIndex = true" do
let inputSchema = [tableA]
let tableAWithCreatedAt = StatementCreateTable (table "a")
{ columns = [
(col "created_at" PTimestampWithTimezone) { defaultValue = Just (CallExpression "NOW" []), notNull = True }
]
}
let index = CreateIndex { indexName = "a_created_at_index", unique = False, tableName = "a", columns = [indexCol (VarExpression "created_at")], whereClause = Nothing, indexType = Nothing }
let expectedSchema = [tableAWithCreatedAt, index]
let options = SchemaOperations.AddColumnOptions
{ tableName = "a"
, columnName = "created_at"
, columnType = PTimestampWithTimezone
, defaultValue = Just (CallExpression "NOW" [])
, isArray = False
, allowNull = False
, isUnique = False
, isReference = False
, referenceTable = Nothing
, primaryKey = False
, withIndex = True
, autoPolicy = False
}
(SchemaOperations.addColumn options inputSchema) `shouldBe` expectedSchema
it "should add a trigger to updated_at columns" do
let inputSchema = [tableA]
let tableAWithCreatedAt = StatementCreateTable (table "a")
{ columns = [
(col "updated_at" PTimestampWithTimezone) { defaultValue = Just (CallExpression "NOW" []), notNull = True }
]
}
let setUpdatedAtFn = (function "set_updated_at_to_now")
{ functionBody = "\nBEGIN\n NEW.updated_at = NOW();\n RETURN NEW;\nEND;\n"
}
let trigger = CreateTrigger
{ name = "update_a_updated_at"
, eventWhen = Before
, event = [TriggerOnUpdate]
, tableName = "a"
, for = ForEachRow
, whenCondition = Nothing
, functionName = "set_updated_at_to_now"
, arguments = []
}
let expectedSchema = [setUpdatedAtFn, tableAWithCreatedAt, trigger]
let options = SchemaOperations.AddColumnOptions
{ tableName = "a"
, columnName = "updated_at"
, columnType = PTimestampWithTimezone
, defaultValue = Just (CallExpression "NOW" [])
, isArray = False
, allowNull = False
, isUnique = False
, isReference = False
, referenceTable = Nothing
, primaryKey = False
, withIndex = False
, autoPolicy = False
}
(SchemaOperations.addColumn options inputSchema) `shouldBe` expectedSchema
it "should add a policy if autoPolicy = true" do
let inputSchema = [tableA]
let tableAWithCreatedAt = StatementCreateTable (table "a")
{ columns = [
(col "user_id" PUUID) { notNull = True }
]
}
let index = CreateIndex
{ indexName = "a_user_id_index"
, unique = False
, tableName = "a"
, columns = [indexCol (VarExpression "user_id")]
, whereClause = Nothing
, indexType = Nothing
}
let constraint = AddConstraint
{ tableName = "a"
, constraint = ForeignKeyConstraint { name = Just "a_ref_user_id", columnName = "user_id", referenceTable = "users", referenceColumn = Just "id", onDelete = Just NoAction }
, deferrable = Nothing
, deferrableType = Nothing
}
let enableRLS = EnableRowLevelSecurity { tableName = "a" }
let p = (policy "Users can manage their a" "a")
{ using = Just (EqExpression (VarExpression "user_id") (CallExpression "ihp_user_id" []))
, check = Just (EqExpression (VarExpression "user_id") (CallExpression "ihp_user_id" []))
}
let expectedSchema = [tableAWithCreatedAt, index, constraint, enableRLS, p]
let options = SchemaOperations.AddColumnOptions
{ tableName = "a"
, columnName = "user_id"
, columnType = PUUID
, defaultValue = Nothing
, isArray = False
, allowNull = False
, isUnique = False
, isReference = True
, referenceTable = Just "users"
, primaryKey = False
, withIndex = False
, autoPolicy = True
}
(SchemaOperations.addColumn options inputSchema) `shouldBe` expectedSchema
describe "deleteColumn" do
it "should delete an referenced index" do
let tableAWithCreatedAt = StatementCreateTable (table "a")
{ columns = [
(col "created_at" PTimestampWithTimezone) { defaultValue = Just (CallExpression "NOW" []), notNull = True }
]
}
let index = CreateIndex { indexName = "a_created_at_index", unique = False, tableName = "a", columns = [indexCol (VarExpression "created_at")], whereClause = Nothing, indexType = Nothing }
let inputSchema = [tableAWithCreatedAt, index]
let expectedSchema = [tableA]
let options = SchemaOperations.DeleteColumnOptions
{ tableName = "a"
, columnName = "created_at"
, columnId = 0
}
(SchemaOperations.deleteColumn options inputSchema) `shouldBe` expectedSchema
it "should delete a updated_at trigger" do
let tableAWithCreatedAt = StatementCreateTable (table "a")
{ columns = [
(col "updated_at" PTimestampWithTimezone) { defaultValue = Just (CallExpression "NOW" []), notNull = True }
]
}
let setUpdatedAtFn = (function "set_updated_at_to_now")
{ functionBody = "\nBEGIN\n NEW.updated_at = NOW();\n RETURN NEW;\nEND;\n"
}
let trigger = CreateTrigger
{ name = "update_a_updated_at"
, eventWhen = Before
, event = [TriggerOnUpdate]
, tableName = "a"
, for = ForEachRow
, whenCondition = Nothing
, functionName = "set_updated_at_to_now"
, arguments = []
}
let inputSchema = [setUpdatedAtFn, tableAWithCreatedAt, trigger]
let expectedSchema = [setUpdatedAtFn, tableA]
let options = SchemaOperations.DeleteColumnOptions
{ tableName = "a"
, columnName = "updated_at"
, columnId = 0
}
(SchemaOperations.deleteColumn options inputSchema) `shouldBe` expectedSchema
it "should delete an referenced policy" do
let tableAWithUserId = StatementCreateTable (table "a")
{ columns = [
(col "user_id" PUUID) { defaultValue = Just (CallExpression "ihp_user_id" []), notNull = True }
]
}
let p = (policy "a_policy" "a") { using = Just (EqExpression (VarExpression "user_id") (CallExpression "ihp_user_id" [])) }
let inputSchema = [tableAWithUserId, p]
let expectedSchema = [tableA]
let options = SchemaOperations.DeleteColumnOptions
{ tableName = "a"
, columnName = "user_id"
, columnId = 0
}
(SchemaOperations.deleteColumn options inputSchema) `shouldBe` expectedSchema
describe "update" do
it "update a column's name, type, default value and not null" do
let tableAWithCreatedAt = StatementCreateTable (table "a")
{ columns = [
(col "updated_at" PTimestampWithTimezone) { defaultValue = Just (CallExpression "NOW" []), notNull = True }
]
}
let tableAWithUpdatedColumn = StatementCreateTable (table "a")
{ columns = [
col "created_at2" PText
]
}
let inputSchema = [tableAWithCreatedAt]
let expectedSchema = [tableAWithUpdatedColumn]
let options = SchemaOperations.UpdateColumnOptions
{ tableName = "a"
, columnName = "created_at2"
, columnType = PText
, defaultValue = Nothing
, isArray = False
, allowNull = True
, isUnique = False
, primaryKey = False
, columnId = 0
}
(SchemaOperations.updateColumn options inputSchema) `shouldBe` expectedSchema
it "updates a primary key" do
let tableWithPK = StatementCreateTable (table "a")
{ columns = [
(col "id2" PUUID) { notNull = True }
]
, primaryKeyConstraint = PrimaryKeyConstraint ["id"]
}
let tableWithoutPK = StatementCreateTable (table "a")
{ columns = [
(col "id" PUUID) { notNull = True }
]
}
let inputSchema = [tableWithoutPK]
let expectedSchema = [tableWithPK]
let options = SchemaOperations.UpdateColumnOptions
{ tableName = "a"
, columnName = "id2"
, columnType = PUUID
, defaultValue = Nothing
, isArray = False
, allowNull = False
, isUnique = False
, primaryKey = True
, columnId = 0
}
(SchemaOperations.updateColumn options inputSchema) `shouldBe` expectedSchema
it "updates referenced foreign key constraints" do
let tasksTable = StatementCreateTable (table "tasks")
{ columns =
[ (col "task_list_id" PUUID) { notNull = True }
]
}
let taskListsTable = StatementCreateTable (table "task_lists")
{ columns =
[ (col "user_id" PUUID) { notNull = True }
]
}
let inputSchema =
[ tasksTable
, taskListsTable
, AddConstraint { tableName = "tasks", constraint = ForeignKeyConstraint { name = "tasks_ref_task_lists", columnName = "task_list_id", referenceTable = "task_lists", referenceColumn = Nothing, onDelete = Nothing }, deferrable = Nothing, deferrableType = Nothing }
]
let tasksTable' = StatementCreateTable (table "tasks")
{ columns =
[ (col "list_id" PUUID) { notNull = True }
]
}
let expectedSchema =
[ tasksTable'
, taskListsTable
, AddConstraint { tableName = "tasks", constraint = ForeignKeyConstraint { name = "tasks_ref_task_lists", columnName = "list_id", referenceTable = "task_lists", referenceColumn = Nothing, onDelete = Nothing }, deferrable = Nothing, deferrableType = Nothing }
]
let options = SchemaOperations.UpdateColumnOptions
{ tableName = "tasks"
, columnName = "list_id"
, columnType = PUUID
, defaultValue = Nothing
, isArray = False
, allowNull = False
, isUnique = False
, primaryKey = False
, columnId = 0
}
(SchemaOperations.updateColumn options inputSchema) `shouldBe` expectedSchema
it "update a column's indexes" do
let tableAWithCreatedAt = StatementCreateTable (table "a")
{ columns = [
(col "updated_at" PTimestampWithTimezone) { defaultValue = Just (CallExpression "NOW" []), notNull = True }
]
}
let index = CreateIndex { indexName = "a_updated_at_index", unique = False, tableName = "a", columns = [indexCol (VarExpression "updated_at")], whereClause = Nothing, indexType = Nothing }
let tableAWithUpdatedColumn = StatementCreateTable (table "a")
{ columns = [
col "created_at" PText
]
}
let indexUpdated = CreateIndex { indexName = "a_created_at_index", unique = False, tableName = "a", columns = [indexCol (VarExpression "created_at")], whereClause = Nothing, indexType = Nothing }
let inputSchema = [tableAWithCreatedAt, index]
let expectedSchema = [tableAWithUpdatedColumn, indexUpdated]
let options = SchemaOperations.UpdateColumnOptions
{ tableName = "a"
, columnName = "created_at"
, columnType = PText
, defaultValue = Nothing
, isArray = False
, allowNull = True
, isUnique = False
, primaryKey = False
, columnId = 0
}
(SchemaOperations.updateColumn options inputSchema) `shouldBe` expectedSchema
describe "updateTable" do
it "renames a table with all it's indices, constraints, policies, enable RLS statements, triggers" do
let inputSchema = parseSqlStatements [trimming|
CREATE TABLE tasks ();
CREATE INDEX tasks_user_id_index ON tasks (user_id);
ALTER TABLE tasks ADD CONSTRAINT tasks_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
CREATE POLICY "Users can manage their tasks" ON tasks USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
CREATE TRIGGER update_tasks_updated_at BEFORE UPDATE ON tasks FOR EACH ROW EXECUTE FUNCTION set_updated_at_to_now();
|]
let outputSchema = parseSqlStatements [trimming|
CREATE TABLE todos ();
CREATE INDEX todos_user_id_index ON todos (user_id);
ALTER TABLE todos ADD CONSTRAINT todos_ref_user_id FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE NO ACTION;
CREATE POLICY "Users can manage their todos" ON todos USING (user_id = ihp_user_id()) WITH CHECK (user_id = ihp_user_id());
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
CREATE TRIGGER update_todos_updated_at BEFORE UPDATE ON todos FOR EACH ROW EXECUTE FUNCTION set_updated_at_to_now();
|]
SchemaOperations.updateTable 0 "todos" inputSchema `shouldBe` outputSchema
parseSqlStatements :: Text -> [Statement]
parseSqlStatements sql =
case Megaparsec.runParser Parser.parseDDL "input" sql of
Left parserError -> error (cs $ Megaparsec.errorBundlePretty parserError) -- For better error reporting in hspec
Right statements -> statements