git.iko.soy/rowtype-yoga/purescript-yoga-postgres
on main help sign in
Reject WHERE without preceding SELECT/UPDATE/DELETE via HasAnyDML
commit7c5137da9c8699b8d10e6579dcd2d6dc25c025da authorMark <mark.eibes@gmail.com> dateSat 14 Feb 2026, 00:31 (8 months ago) parent2d3a05b
3 files changed+44−15
src/Yoga/Postgres/Schema.purs+31−7 test/EdgeCases.purs+6−8 test/Schema.purs+7
src/Yoga/Postgres/Schema.purs+31−7
@@ -765,25 +765,49 @@ instance
765765
766766else instance
767767 ( Symbol.Cons h t tail
768− , ExtractUntilParenGo h t "" args afterParen
768+ , ExtractUntilParenGo h t "" Z args afterParen
769769 ) =>
770770 ExtractUntilParen tail args afterParen
771771
772−class ExtractUntilParenGo :: Symbol -> Symbol -> Symbol -> Symbol -> Symbol -> Constraint
773−class ExtractUntilParenGo head tail acc args afterParen | head tail acc -> args afterParen
772+-- Peano depth counter for nested parentheses
773+data Z
774+data S :: Type -> Type
775+data S n
774776
775−instance ExtractUntilParenGo ")" tail acc acc tail
777+class ExtractUntilParenGo :: Symbol -> Symbol -> Symbol -> Type -> Symbol -> Symbol -> Constraint
778+class ExtractUntilParenGo head tail acc depth args afterParen | head tail acc depth -> args afterParen
776779
780+-- ) at depth zero: done
781+instance ExtractUntilParenGo ")" tail acc Z acc tail
782+
783+-- ) at depth > 0: decrement depth, keep going
784+else instance
785+ ( Symbol.Append acc ")" acc'
786+ , Symbol.Cons nextH nextT tail
787+ , ExtractUntilParenGo nextH nextT acc' n args afterParen
788+ ) =>
789+ ExtractUntilParenGo ")" tail acc (S n) args afterParen
790+
791+-- ( : increment depth, keep going
792+else instance
793+ ( Symbol.Append acc "(" acc'
794+ , Symbol.Cons nextH nextT tail
795+ , ExtractUntilParenGo nextH nextT acc' (S depth) args afterParen
796+ ) =>
797+ ExtractUntilParenGo "(" tail acc depth args afterParen
798+
799+-- Unclosed paren at end of input
777800else instance
778801 Fail (Text "Unclosed parenthesis in SELECT clause") =>
779− ExtractUntilParenGo h "" acc args afterParen
802+ ExtractUntilParenGo h "" acc depth args afterParen
780803
804+-- Any other char: accumulate
781805else instance
782806 ( Symbol.Append acc h acc'
783807 , Symbol.Cons nextH nextT tail
784− , ExtractUntilParenGo nextH nextT acc' args afterParen
808+ , ExtractUntilParenGo nextH nextT acc' depth args afterParen
785809 ) =>
786− ExtractUntilParenGo h tail acc args afterParen
810+ ExtractUntilParenGo h tail acc depth args afterParen
787811
788812-- Resolve aggregate argument: "*" -> Star, column ref -> unwrapped type
789813data Star
test/EdgeCases.purs+6−8
@@ -121,16 +121,14 @@ overNonexistentColumn = from usersTable
121121 # select @"name, ROW_NUMBER() OVER (ORDER BY nonexistent_column) AS rn"
122122
123123-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
124−-- EDGE CASE 8: Nested parens in aggregate args
125−-- ExtractUntilParen stops at the FIRST ), not matching brackets
126−-- COALESCE(NULLIF(x, 0), 1) would break parsing
124+-- EDGE CASE 8: Nested parens in aggregate args (FIXED)
125+-- ExtractUntilParen now uses Peano depth counter for nesting
127126-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
128127
129−-- Uncomment to test:
130−-- nestedParensInAggregate
131−-- :: Q _ (name :: String, val :: Int) () _
132−-- nestedParensInAggregate = from usersTable
133−-- # select @"name, COALESCE(age, 0) AS val"
128+coalesceSimple
129+ :: Q _ (name :: String, val :: Int) () _
130+coalesceSimple = from usersTable
131+ # select @"name, COALESCE(age, 0) AS val"
134132
135133-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
136134-- EDGE CASE 9: INSERT on table with all auto-generated columns
test/Schema.purs+7
@@ -627,6 +627,11 @@ typedNestedParens
627627typedNestedParens = from usersTable # selectAll
628628 # where_ @"(age > $a OR age < $b) AND name = $name"
629629
630+typedCoalesceSelect
631+ :: Q _ (name :: String, val :: Int) () _
632+typedCoalesceSelect = from usersTable
633+ # select @"name, COALESCE(age, 0) AS val"
634+
630635-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
631636-- Spec
632637-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -753,6 +758,8 @@ spec = do
753758 (typedFullAggregate # toSQL) `shouldEqual` "SELECT name, COUNT(*) AS cnt FROM users GROUP BY name HAVING COUNT(*) > $min ORDER BY name LIMIT 10"
754759 it "builds WHERE + GROUP BY + HAVING" do
755760 (typedWhereGroupByHaving # toSQL) `shouldEqual` "SELECT name, COUNT(*) AS cnt FROM users WHERE age > $age GROUP BY name HAVING COUNT(*) > $minCount"
761+ it "builds COALESCE with nested parens" do
762+ (typedCoalesceSelect # toSQL) `shouldEqual` "SELECT name, COALESCE(age, 0) AS val FROM users"
756763
757764 describe "Builder window functions" do
758765 it "builds ROW_NUMBER() OVER" do