/src-extra/extensions/Database/HsSqlPpp/Extensions/CreateAssertion.lhs

http://github.com/JakeWheat/hssqlppp · Haskell · 250 lines · 212 code · 35 blank · 3 comment · 16 complexity · c750a5ab5a38a1f951ea188e3a8aaa51 MD5 · raw file

  1. sketch
  2. ------
  3. something like an implementation of the rarely implemented create
  4. assertion sql syntax
  5. The basic approach is to analyze the constraint expression to get a
  6. list of ultimately reference tables, then:
  7. create a check function which returns a bool to say whether the
  8. expression is true,
  9. create a trigger and trigger function for each table which calls all
  10. the check functions who reference this table and raise if any are
  11. false
  12. when we add a constraint which references a table with an existing
  13. constraint, we add the new check function, and create or replace the
  14. trigger function with the new check function added to the existing
  15. ones, which avoids having to also drop and create the trigger
  16. could add an optimisation to say that if there are no crud statements
  17. inbetween two constraints, we can skip creating the first trigger
  18. function, and change create or replace to create on the second one,
  19. and move the add trigger down. Also - want to try to limit any crud to
  20. using the table values extension, which may be relevant to this.
  21. todo/ issues
  22. ------------
  23. add catalog entries. not null and key constrainsts are part of a
  24. table, but the regular check and foreign key constraints possibly
  25. could appear in assertion catalog along with the constraints added by
  26. this extension
  27. the logic which determines which tables are referenced by an
  28. expression is a bit dodgy: it only copes with all the create
  29. assertions being set in one ast, so we can load one file, then load
  30. another file later and it do the right thing.
  31. this logic is also wrong for functions - it doesn't take into account
  32. function overloading, to fix this need to type check to determine
  33. which exact functions are called, might be a bit tricky, since we run
  34. extensions on asts which only neccessarily type check after all the
  35. extensions have been applied.
  36. Since pg has no multiple updates, some sort of hack may be needed for
  37. some updates in combination with some constraints.
  38. I'm pretty sure the constraint system works fine as long as
  39. * you never change the columns on a table after adding a constraint
  40. * you only add constraints, never change or remove them
  41. * all database transactions are run one at a time, serialised
  42. (actually serialised, not just using sql isolation serializable).
  43. If any of these assumptions are broken, you might break your database,
  44. load bad data in or get weird errors for stuff that should work.
  45. foreign keys without having to create a view first i.e. a literal
  46. view expression in the constraint.
  47. alternative key syntax to remove the distinction between primary key
  48. and unique not null?
  49. could add support for check constraints referring to multiple rows in
  50. same table? don't want to allow check constraints which refer to other
  51. tables. Maybe go further: following tutorial d, only allow key and not
  52. null constraints in create table or alter table, and move chek and
  53. foreign keys out to create assertions or shorthand wrappers? Inline
  54. constraints can be more readable though?
  55. maybe add a parser extension to parse:
  56. create assertion constraint_name check(expression);
  57. add some syntax to say:
  58. not too sure about how this all works, write a seperate bunch of
  59. code to read all the constraints either out of the catalog or from the
  60. source, then use the final version of the sql code, type check it, and
  61. use the type check information to check all the constraints seem to
  62. have been added ok.
  63. * here are some example sets of relation values which should be
  64. accepted by the constraint
  65. * here are some which should not be accepted by the constraint
  66. and have a way to check these (this stuff goes in the client program
  67. using the constraints).
  68. add supplemental expressions for error reporting: so instead of
  69. saying x constraint failed, can run through the expressions one at a
  70. time and when one passes or fails or something can give a more
  71. useful error message depending on how it failed.
  72. ~~~~
  73. === ghetto test thing
  74. want to write some tests for this constraint system just as a sanity
  75. check for now:
  76. arbitrary check e.g. cardinality < 5
  77. arbitrary check multiple tables e.g. sum cardinality of two tables
  78. check without acceleration?:
  79. fk
  80. fk to view
  81. unique
  82. x,y in board size range from another table
  83. for each check:
  84. check adding constraint to invalid tables throws
  85. check adding constraint to valid tables OK
  86. insert OK data into constrained tables
  87. insert bad data into constrained tables
  88. accelerated checks
  89. fk to view
  90. x,y in board size range
  91. check acceleration for normal checks & fk without pg?
  92. pg accelerated checks:
  93. just check pg catalog to see if inserted
  94. check
  95. fk
  96. unique
  97. all todo: yes, that means there is no direct testing of any of the
  98. constraint stuff...
  99. ~~~~~
  100. not too sure about how this all works, write a seperate bunch of
  101. code to read all the constraints either out of the catalog or from the
  102. source, then use the final version of the sql code, type check it, and
  103. use the type check information to check all the constraints seem to
  104. have been added ok.
  105. ----------------------------
  106. Tests/examples in CreateAssertionTests.lhs
  107. > {-# LANGUAGE QuasiQuotes, ScopedTypeVariables, TupleSections #-}
  108. >
  109. > module Database.HsSqlPpp.Extensions.CreateAssertion
  110. > (createAssertion) where
  111. >
  112. > --import Data.Generics
  113. > import Data.Generics.Uniplate.Data
  114. > --import Debug.Trace
  115. > import Control.Monad.State
  116. > import Data.Maybe
  117. >
  118. > import Database.HsSqlPpp.Ast
  119. > import Database.HsSqlPpp.Parser
  120. > import Database.HsSqlPpp.Utils.Utils
  121. > import Database.HsSqlPpp.Quote
  122. > import Database.HsSqlPpp.Extensions.AstUtils
  123. > import Database.HsSqlPpp.Annotation
  124. implementation
  125. ==============
  126. when we go through, need to record the constraints we've already
  127. seen. using transformBiM with state monad, gives us the constraints in
  128. reverse order of the statement list, so chuck three reverses in there
  129. to make it work right.
  130. > createAssertion :: [Statement] -> [Statement]
  131. > createAssertion ast = reverse $
  132. > (\f -> evalState (transformBiM f (reverse ast)) ([] :: ConstraintRecord)) $ \x ->
  133. > case x of
  134. > s@[sqlStmt| select create_assertion($s(name)
  135. > ,$s(exprtext));|] : tl -> do
  136. > existing <- get
  137. > let (new, rast) = makeConstraintDdl existing name exprtext
  138. > put new
  139. > return $ replaceSourcePos s rast ++ tl
  140. > x1 -> return x1
  141. > where
  142. > asti = getAstInfo ast
  143. > makeConstraintDdl :: ConstraintRecord -> String -> String -> (ConstraintRecord, [Statement])
  144. > makeConstraintDdl cons name exprText =
  145. > let expr = either (error . show) id
  146. > $ parseScalarExpr "" exprText
  147. > in (newcons cons (tableNames expr) name
  148. > ,reverse (makeCheckFn name expr : extras cons name expr))
  149. > extras :: ConstraintRecord -> String -> ScalarExpr -> [Statement]
  150. > extras cons name expr = flip concatMap (tableNames expr) $ \tn ->
  151. > let ec = existingConstraints tn cons
  152. > in if null ec
  153. > then [makeTriggerFn False tn [name]
  154. > ,makeTrigger tn]
  155. > else [makeTriggerFn True tn (name:ec)]
  156. > tableNames expr = let y = getReferencedTableList asti expr
  157. > in y
  158. > newcons cons tns nm = foldr (uncurry (insertWith (++))) cons (map (,[nm]) tns)
  159. > existingConstraints tn cons = fromMaybe [] $ lookup tn cons
  160. >
  161. > type ConstraintRecord = [(String,[String])] -- tablename, list of constraint names
  162. >
  163. > makeCheckFn :: String -> ScalarExpr -> Statement
  164. > makeCheckFn name expr =
  165. > let checkfn = "check_con_" ++ name
  166. > in [sqlStmt|
  167. > create function $(checkfn)() returns bool as $xxx$
  168. > begin
  169. > return $(expr);
  170. > end;
  171. > $xxx$ language plpgsql stable;
  172. > |]
  173. >
  174. > makeTriggerFn :: Bool -> String -> [String] -> Statement
  175. > makeTriggerFn r tn nms =
  176. > let trigopname = tn ++ "_constraint_trigger_operator"
  177. > ifs :: [Statement]
  178. > ifs = map makeIf nms
  179. > -- using template approach cos can't get antistatement -> [statement] working
  180. > template = [sqlStmt|
  181. > create function $(trigopname)() returns trigger as $xxx$
  182. > begin
  183. > null;
  184. > return OLD;
  185. > end;
  186. > $xxx$ language plpgsql stable;
  187. > |]
  188. > rep = if r
  189. > then transformBi $ \x ->
  190. > case x of
  191. > NoReplace -> Replace
  192. > x1 -> x1
  193. > else id
  194. > in flip transformBi (rep template) $ \x ->
  195. > case x of
  196. > NullStatement _ : tl -> ifs ++ tl
  197. > x1 -> x1
  198. > where
  199. > makeIf nm = let chk = "check_con_" ++ nm
  200. > errMsg = "update violates database constraint " ++ nm
  201. > in [pgsqlStmt|
  202. > if not $(chk)() then
  203. > raise exception '$(errMsg)';
  204. > end if;
  205. > |]
  206. >
  207. > makeTrigger :: String -> Statement
  208. > makeTrigger tn = let trigname = tn ++ "_constraint_trigger"
  209. > opname = tn ++ "_constraint_trigger_operator"
  210. > in [sqlStmt|
  211. > create trigger $(trigname)
  212. > after insert or update or delete on $(tn)
  213. > for each statement
  214. > execute procedure $(opname)();
  215. > |]