### ----------------------------------------------------
### -- SQL::Abstract
### -- Licenses: Artistic-2.0
### -- Authors: Leon Timmermans
### -- File: lib/SQL/Abstract.rakumod
### ----------------------------------------------------
### -- Chunk 2 of 4
-expression($value);
			Boundary::Preceding::Bounded.new(:$offset);
		}
		multi method COERCE(Pair (Str :$key where 'following', Any :$value)) {
			my $offset = expand-expression($value);
			Boundary::Following::Bounded.new(:$offset);
		}
	}
	role Boundary::Unbounded[Str $tag] does Boundary {
		method tag() { $tag }
	}
	role Boundary::Bounded[Str $tag] does Boundary {
		method tag() { $tag }

		has Expression:D $.offset is required;
	}
	class Boundary::Preceding::Unbounded does Boundary::Unbounded['UNBOUNDED PRECEDING'] {}
	class Boundary::Preceding::Bounded does Boundary::Bounded['PRECEDING'] {}
	class Boundary::CurrentRow does Boundary::Unbounded['CURRENT ROW'] {}
	class Boundary::Following::Bounded does Boundary::Bounded['FOLLOWING'] {}
	class Boundary::Following::Unbounded does Boundary::Unbounded['UNBOUNDED FOLLOWING'] {}

	class Frame {
		has Window::Mode:D $.mode is required;
		has Boundary:D $.from is required;
		has Boundary $.to;
		has Exclusion:D $.exclude is required;
	}

	class Definition {
		has Identifier   $.existing;
		has Column::List $.partition-by;
		has OrderBy      $.order-by;
		has Frame        $.frame;

		method is-simple() {
			$!existing && !$!partition-by && !$!order-by && !$!frame;
		}

		method COERCE(Map (Column::List(Any) :$partition-by, OrderBy(Any) :$order-by, Identifier(Any) :$existing = Identifier, Boundary(Any) :$from = Boundary::Preceding::Unbounded.new, Boundary(Any) :$to, Exclusion(Str) :$exclude = Exclusion::NoOthers, Mode(Str) :$mode = Mode::Range)) {
			my $default-frame = $from ~~ Boundary::Preceding::Unbounded && !$to && $mode === Mode::Range && $exclude === Exclusion::NoOthers;
			my $frame = $default-frame ?? Frame !! Frame.new(:$mode, :$from, :$to, :$exclude);
			self.new(:$existing, :$partition-by, :$order-by, :$frame);
		}
	}

	class Clause {
		has Identifier:D         $.name is required;
		has Window::Definition:D $.definition is required;

		method COERCE(Pair (Identifier(Any) :$key, Window::Definition(Map) :$value)) {
			self.new(:name($key), :definition($value));
		}
	}

	class Clauses {
		has Window::Clause @.windows;

		multi method COERCE(@specs) {
			my Window::Clause(Pair) @windows = @specs;
			self.new(:@windows);
		}
		multi method COERCE(Window::Clause(Pair) $window) {
			self.new(:windows[ $window ]);
		}

		multi method merge(Clauses:U: Clauses $other) {
			$other;
		}
		multi method merge(Clauses:D: Clauses:U $other) {
			self;
		}
		multi method merge(Clauses:D: Clauses:D $other) {
			my @windows = |@!windows, |$other.windows;
			self.new(:@windows);
		}
	}
}

class Window::Function does Expression {
	method precedence(--> Precedence::Comma) {}

	has Function:D $.function is required;
	has Window::Definition:D $.window is required;
}

class Locking {
	enum Strength (:Update<update> :NoKeyUpdate('no key update') :Share<share> :KeyShare('key share'));

	class Clause {

		has Strength:D $.strength is required;
		has Identifiers $.tables;
		has Bool $.no-wait;
		has Bool $.skip-locked;

		multi method COERCE(Strength(Str) $strength) {
			self.new(:$strength);
		}
		multi method COERCE(Pair (Strength(Str) :$key, Identifiers(Cool) :$value)) {
			self.new(:strength($key), :tables($value));
		}
		multi method COERCE(Map (Strength(Str) :$strength, Identifiers(Cool) :$tables, Bool :$no-wait, Bool :$skip-locked?)) {
			self.new(:$strength, :$tables, :$no-wait, :$skip-locked);
		}
	}

	has Clause @.lockings;

	multi method COERCE(@specs) {
		my Clause(Any) @lockings = @specs;
		self.new(:@lockings);
	}
	multi method COERCE(Clause(Any) $locking) {
		self.new(:lockings[ $locking ]);
	}
}

class Compound {
	enum Type <Union Intersect Except>;
	has Type:D $.type = Type::Union;
	has Bool $.all;
	has Select $.next;

	multi method COERCE(Pair $ (Str :$key, Select(Map) :$value)) {
		my $operation = $key.lc;
		my $all = so $operation ~~ s/ '-all' $ //;
		my $type = Type.WHO{$operation.lc.tc};
		self.new(:$type, :next($value), :$all);
	}
}

class Limit {
	class All does Constant['ALL'] {}

	has Expression:D $.value is required;

	multi method COERCE(Expression $value) {
		self.new(:$value);
	}
	multi method COERCE(Placeholder(Any) $value) {
		self.new(:$value);
	}
	multi method COERCE(Inf) {
		self.new(:value(All.new));
	}
}

class Offset {
	has Expression:D $.value is required;

	multi method COERCE(Expression $value) {
		self.new(:$value);
	}
	multi method COERCE(Placeholder(Any) $value) {
		self.new(:$value);
	}
}

class Source::Query { ... }
role Query does Expression {
	method precedence(--> Precedence::Rowlike) {}

	method create(|) { ... }

	multi method COERCE(%arguments) {
		self.create(|%arguments);
	}

	method as-source(Identifier:D(Any:D) $alias, Bool :$lateral) {
		Source::Query.new(:query(self), :$alias, :$lateral);
	}

	method but(%args) {
		self.new(|self.Capture.hash, |%args);
	}
}

class Values does Query {
	has Rows:D  $.rows is required;
	has OrderBy $.order-by;
	has Limit   $.limit;
	has Offset  $.offset;

	method create(Rows(Any) :$rows!, OrderBy(Any) :$order-by, Limit(Any) :$limit, Offset(Any) :$offset) {
		self.new(:$rows, :$order-by, :$limit, :$offset);
	}
}

role Table { ... }

class Common {
	class Name {
		has Identifier:D $.name is required;
		has Identifiers $.columns;

		multi method COERCE(Identifier(Any) $name) {
			self.new(:$name);
		}
		multi method COERCE(Pair $pair (Identifier(Any) :key($name), Identifiers(Cool) :value($columns))) {
			self.new(:$name, :$columns);
		}
	}
	class Rename {
		has Name:D $.alias is required;
		has Query:D $.query is required;
		has Bool $.materialized;

		multi method COERCE(Pair $pair (Name(Any) :key($alias), Select(Map) :value($query))) {
			self.new(:$alias, :$query);
		}
		multi method COERCE(Pair $pair (Name(Any) :key($alias), Table(Str) :value($from))) {
			my $query = Select.create(:$from);
			self.new(:$alias, :$query);
		}
		multi method COERCE(Pair $pair (Name(Any) :key($alias), Query :value($query))) {
			self.new(:$alias, :$query);
		}
		multi method COERCE(Map (Name(Any) :$alias, Select(Map) :$query, Bool :$materialized)) {
			self.new(:$alias, :$query, :$materialized);
		}
		multi method COERCE(Map (Name(Any) :$alias, Table(Str) :$table, Bool :$materialized)) {
			my $query = Select.create(:from($table));
			self.new(:$alias, :$query, :$materialized);
		}
		multi method COERCE(Map (Name(Any) :$alias, Query :$query, Bool :$materialized)) {
			self.new(:$alias, :$query, :$materialized);
		}
	}
	has Rename @.tables;
	has Bool $.recursive;

	multi method COERCE(Rename(Any) $table) {
		self.new(:tables[$table]);
	}
	multi method COERCE(@pairs) {
		my Rename(Any) @tables = @pairs;
		self.new(:@tables);
	}

	multi method merge(Common:U: Common $other) {
		$other;
	}
	multi method merge(Common:U: Common:U $other) {
		self;
	}
	multi method merge(Common:D: Common:D $other) {
		my @tables = |@!tables, |$other.tables;
		my $recursive = $!recursive || $other.recursive;
		self.new(:@tables, :$recursive);
	}
}

role Query::Common does Query {
	has Common(Any)      $.common;
}

class Distinction::Full { ... }
class Distinction::Columns { ... }

role Distinction {
	multi method COERCE(Bool $value) {
		$value ?? Distinction::Full.new !! Distinction;
	}
	multi method COERCE(Column::List(Any) $columns) {
		Distinction::Columns.new(:$columns);
	}
}

class Distinction::Full does Distinction {}

class Distinction::Columns does Distinction {
	has Column::List $.columns;
}

class GroupBy {
	has Expression:D @.elements;
	has Bool $.all;

	multi to-grouping(Any $column) {
		Column::List::to-column($column);
	}
	multi to-grouping(Column::List(List) $column) {
		$column;
	}
	multi to-grouping(Function(Pair) $function) {
		$function;
	}
	multi to-grouping(Pair $pair (:$key where $key.lc eq 'grouping sets', :$value)) {
		my @values = $value.map(&to-grouping);
		Function.COERCE(( $key => @values ));
	}

	multi method COERCE(Any $grouping) {
		self.new(:elements[to-grouping($grouping)]);
	}
	multi method COERCE(@list) {
		my @elements = @list.map(&to-grouping);
		self.new(:@elements);
	}
	multi method COERCE(Identifiers $elements) {
		self.new(:elements($elements.elements));
	}
	multi method COERCE(Column::List $elements) {
		self.new(:elements($elements.elements));
	}
	multi method COERCE(Map $ (Bool :$all, :@elements)) {
		self.new(:$all, :elements(@elements.map(&to-grouping)));
	}
}

role Source { ... }

class Select does Query::Common {
	has Column::List:D  $.columns is required;
	has Distinction     $.distinct;
	has Source          $.from;
	has Conditions      $.where;
	has GroupBy         $.group-by;
	has Conditions      $.having;
	has Window::Clauses $.windows;
	has Compound        $.compound;

	has OrderBy         $.order-by;
	has Limit           $.limit;
	has Offset          $.offset;
	has Locking         $.locking;

	method create(Source(Any) :$from!, Column::List(Any) :$columns = *, Conditions(Any) :$where, Common(Any) :$common, Distinction(Any) :$distinct, GroupBy(Any) :$group-by, Conditions(Any) :$having, Window::Clauses(Any) :$windows, Compound(Pair) :$compound, OrderBy(Any) :$order-by, Limit(Any) :$limit, Offset(Any) :$offset, Locking(Any) :$locking) {
		Select.new(:$common, :$distinct, :$columns, :$from, :$where, :$group-by :$having, :$windows, :$compound, :$order-by, :$limit, :$offset, :$locking);
	}
}

multi expand-capture(Select(Map) :$select!) {
	$select;
}

class Join::Conditions { ... }

class Update::From {
	has Source:D $.source is required;
	has Join::Conditions $.conditions;

	multi method COERCE(Source(Str) $source) {
		self.new(:$source);
	}
	multi method COERCE(Pair (Source(Any) :$key, Join::Conditions(Any) :$value)) {
		self.new(:source($key), :conditions($value));
	}
}

class Update does Query::Common {
	has Table:D      $.target is required;
	has Op::Assign:D @.assignments is required;
	has Update::From $.from;
	has Conditions   $.where;
	has Column::List $.returning;

	method create(Table(Cool) :$target!, Assigns:D(Any:D) :$assigns!, Update::From(Any) :$from, Conditions(Any) :$where, Column::List(Any) :$returning) {
		my @assignments = $assigns.assignments;
		Update.new(:$target, :@assignments, :$where, :$from, :$returning);
	}
}

enum Overriding (:System<system>, :User<user>);

class Conflict::Target::Columns { ... }
class Conflict::Target::Constraint { ... }

role Conflict::Target {
	multi method COERCE(Identifiers(Cool) $columns) {
		Conflict::Target::Columns.new(:$columns);
	}
	multi method COERCE(Pair (:$key where 'columns', Identifiers(Cool) :$value)) {
		Conflict::Target::Columns.new(:columns($value));
	}
	multi method COERCE(Pair (:$key where 'columns', Map :value($) (Identifiers(Cool) :$columns!, Conditions(Any) :$where))) {
		Conflict::Target::Columns.new(:$columns, :$where);
	}
	multi method COERCE(Pair (:$key where 'constraint', Identifier(Any) :$value)) {
		Conflict::Target::Constraint.new(:constraint($value));
	}
}

class Conflict::Target::Columns does Conflict::Target {
	has Identifiers:D $.columns is required;
	has Conditions    $.where;
}

class Conflict::Target::Constraint does Conflict::Target {
	has Identifier:D $.constraint is required;
}

class Conflict::Targets {
	has Conflict::Target @.targets;

	multi method COERCE(@specs) {
		my Conflict::Target(Any) @targets = @specs;
		self.new(:@targets);
	}
	multi method COERCE(Conflict::Target(Any) $conflict) {
		self.new(:targets[ $conflict ]);
	}
}

class Conflict::Nothing { ... }

class Conflict::Update { ... }

role Conflict {
	has Conflict::Target $.target;

	multi method COERCE('nothing') {
		Conflict::Nothing.new;
	}
	multi method COERCE(Pair (Conflict::Target(Any) :$key, Str :$value where 'nothing')) {
		Conflict::Noting.new(:target($key));
	}
	multi method COERCE(Pair (Conflict::Target(Any) :$key, Assigns(Any) :$value)) {
		Conflict::Update.new(:target($key), :assignments($value));
	}
	multi method COERCE(Map (Conflict::Target(Any) :$target?, Str :$do! where 'nothing')) {
		Conflict::Nothing.new(:$target);
	}
	multi method COERCE(Map (Conflict::Target(Any) :$target!, Assigns(Any) :$do!, Conditions(Any) :$where)) {
		Conflict::Update.new(:$target, :assignments($do), :$where);
	}
}

class Conflict::Nothing does Conflict {}

class Conflict::Update does Conflict {
	has Op::Assign @.assignments;
	has Conditions $.where;

	submethod BUILD(Conflict::Target :$!target!, Assigns :$assignments!, Conditions :$!where) {
		@!assignments = $assignments.assignments;
	}
}

class Conflicts {
	has Conflict @.conflicts;

	multi method COERCE(@specs) {
		my Conflict(Any) @conflicts = @specs;
		self.new(:@conflicts);
	}
	multi method COERCE(Conflict(Any) $conflict) {
		self.new(:conflicts[ $conflict ]);
	}
}

role Insert does Query::Common {
	has Table:D      $.target is required;
	has Identifiers  $.columns;
	has Overriding   $.overriding;
	has Conflicts    $.conflicts;
	has Column::List $.returning;
}

class Insert::Values does Insert {
	has Rows:D $.rows is required;

	method create(Table(Any) :$target!, Identifiers(Cool) :$columns, Rows(List) :$rows!, Overriding(Str) :$overriding, Conflicts(Any) :$conflicts, Column::List(Any) :$returning) {
		Insert::Values.new(:$target, :$columns, :$rows, :$overriding, :$conflicts, :$returning);
	}
}

class Insert::Select does Insert {
	has Select:D $.select is required;

	method create(Table(Any) :$target!, Identifiers(Cool) :$columns, Select(Map) :$select!, Overriding(Str) :$overriding, Conflicts(Any) :$conflicts, Column::List(Any) :$returning) {
		Insert::Select.new(:$target, :$columns, :$select, :$overriding, :$conflicts, :$returning);
	}
}

class Insert::Defaults does Insert {
	method create(Table(Any) :$target!, Overriding(Str) :$overriding, Conflicts(Any) :$conflicts, Column::List(Any) :$returning) {
		Insert::Defaults.new(:$target, :$overriding, :$conflicts, :$returning);
	}
}

class Delete does Query::Common {
	has Table:D      $.target is required;
	has Update::From $.using;
	has Conditions   $.where;
	has Column::List $.returning;

	method create(Table(Cool) :$target!, Update::From(Any) :$using, Conditions(Any) :$where, Column::List(Any) :$returning) {
		Delete.new(:$target, :$where, :$using :$returning);
	}
}

class Join::On { ... }
class Join::Using { ... }
class Join::Natural { ... }
class Join::Cross { ... }
class Table::Simple { ... }
class Table::Renamed { ... }

enum Join::Type (:Inner<inner>, :Left<left>, :Right<right>, :Full<full>);

role Source {
	multi method COERCE(Identifier(Str) $name) is default {
		Table::Simple.new(:$name);
	}

	my subset FunctionMap of Map where .EXISTS-KEY('function');
	multi method COERCE(Pair (Identifier(Any) :$key, Identifier(Str) :$value)) {
		Table::Renamed.new(:name($value), :alias($key));
	}
	multi method COERCE(Pair (Identifier(Any) :$key, Query :$value)) {
		Source::Query.new(:query($value), :alias($key));
	}
	multi method COERCE(Pair (Identifier(Any) :$key, Select(Map) :$value)) {
		Source::Query.new(:query($value), :alias($key));
	}
	multi method COERCE(Pair (Identifier(Any) :$key, Function:D(FunctionMap) :$value)) {
		Source::Function.new(:function($value), :alias($key));
	}
	multi method COERCE(Pair (Identifier(Any) :$key, Function(Pair) :$value)) {
		Source::Function.new(:function($value), :alias($key));
	}
	multi method COERCE(Pair (Identifier(Any) :$key, Capture :$value)) {
		self.COERCE(($key => expand-capture(|$value)));
	}
	
	multi method COERCE(Map (Identifier(Any) :table($name), Identifier(Any) :$alias, Identifiers(Cool) :$columns, Bool :$only)) {
		Table::Renamed.new(:$name, :$alias, :$columns, :$only);
	}
	multi method COERCE(Map (Identifier(Any) :table($name)!, Bool :$only)) {
		Table::Simple.new(:$name, :$only);
	}
	multi method COERCE(Map (Function(Map) :$function, Identifier(Any) :$alias, Identifiers() :$columns, Bool :$lateral, Bool :$ordinal)) {
		Source::Function.new(:$function, :$alias, :$columns, :$lateral, :$ordinal);
	}
	multi method COERCE(Map (Select(Map) :$query, Identifier(Any) :$alias, Identifiers() :$columns, Bool :$lateral)) {
		Source::Query.new(:query($query), :$alias, :$columns, :$lateral);
	}
	multi method COERCE(Map (Source(Any) :$left!, Source(Any) :$right!, *%args)) {
		$left.join($right, |%args);
	}

	multi method join(Source(Any) $right, Join::Conditions(Any) :$on!, Join::Type(Str) :$type = Join::Type::Inner) {
		Join::On.new(:left(self), :$right, :$on, :$type);
	}
	multi method join(Source(Any) $right, Identifiers(Any) :$using!, Join::Type(Str) :$type = Join::Type::Inner) {
		Join::Using.new(:left(self), :$right, :$using, :$type);
	}
	multi method join(Source(Any) $right, Bool :$natural!, Join::Type(Str) :$type = Join::Type::Inner) {
		Join::Natural.new(:left(self), :$right, :$type);
	}
	multi method join(Source(Any) $right, Bool :$cross!) {
		Join::Cross.new(:left(self), :$right);
	}
}

role Source::Named does Source {
	method alias() { ... }
	
	multi method concat(Identifier $column) {
		self.alias.concat($column);
	}
	multi method concat-all(Identifiers $columns) {
		$columns.elements.map: { self.concat($^column) };
	}
}

role Table does Source {
	has Identifier:D $.name is required;
	has Bool         $.only;
}

class Table::Simple does Table does Source::Named {
	method alias() { $!name }
}

role Source::Aliased does Source::Named {
	has Identifier:D $.alias is required;
	has Identifiers $.columns;
}

class Table::Renamed does Table does Source::Aliased {}

role Join does Source {
	has Source:D $.left is required;
	has Source:D $.right is required;
}

class Join::Conditions does Conditional {
	sub expand(Pair (Identifier(Any) :key($left), Identifier(Cool) :value($right))) {
		Op::Equals.new(:$left, :$right);
	}

	multi method COERCE(Conditions $conditions) {
		self.bless(:expression($conditions.expression));
	}
	multi method COERCE(Pair $pair) {
		self.new(:expressions[ expand($pair) ]);
	}
	multi method COERCE(@list) {
		self.new(:expressions(@list.map(&expand)));
	}
	multi method COERCE(%hash) {
		samewith(%hash.sort(*.key));
	}

	method from-using(Source::Named $left, Source::Named $right, Identifiers $using) {
		my @expressions = $using.elements.map({ Op::Equals.new(:left($left.concat($^item)), :right($right.concat($^item))) });
		self.new(:@expressions);
	}
}
class Join::On does Join {
	has Join::Type:D $.type is required;
	has Join::Conditions $.on;
}

class Join::Using does Join {
	has Join::Type:D $.type is required;
	has Identifiers $.using;

	method to-join-on() {
		my $on = Join::Conditions.from-using($!left, $!right, $!using);
		Join::On.new(:$!left, :$!right, :$!type, :$on);
	}
}

class Join::Natural does Join {
	has Join::Type:D $.type is required;
}

class Join::Cross does Join {}

role Source::Nested does Source::Aliased {
	has Bool $.lateral;
}

class Source::Query does Source::Nested {
	has Query:D $.query is required;
}

class Source::Function does Source::Nested {
	has Function:D $.function is required;
	has Bool       $.ordinal;
}

my role Query::Proxy does Expression {
	method query(--> Query) { ... }

	method precedence(--> Precedence) {
		self.query.precedence;
	}
	method negate(--> Expression) {
		self.query.negate;
	}
	method as(Identifier(Any) $alias) {
		self.query.as($alias);
	}
	method as-source(Identifier:D(Any:D) $alias, Bool :$lateral) {
		self.query.as-source($alias, :$lateral);
	}
}

role Exception is ::Exception {
	has Str:D $.message is required;

	method new(Str:D $message) {
		self.bless(:$message);
	}
}

class Builder { ... }

role Renderer {
	method render() { ... }
	method builder() {
		Builder.new(:renderer(self));
	}
}

role Renderer::SQL { ... }

role Expression::Custom does Expression {
	method render-sql(Renderer::SQL $renderer) { ... }
}

role Renderer::SQL does Renderer {
	has Bool:D $.quoting = False;

	method placeholders() { ... }

	sub parenthesize-if(Str $input, Bool() $parenthese --> Str) {
		$parenthese ?? "($input)" !! $input;
	}
	sub parenthesize-list(@items, Str $with = " ") {
		"({ @items.join($with) })";
	}

	method render-expressions(Placeholders $placeholders, @elements, Precedence $precedence = Precedence::Comma, Str $with = ', ') {
		@elements.map({ self.render-expression($placeholders, $^element, $precedence) }).join($with);
	}

	method render-expression-list(Placeholders $placeholders, @elements, Bool $parenthesize --> Str) {
		my $result = self.render-expressions($placeholders, @elements, Precedence::Comma);
		parenthesize-if($result, $parenthesize);
	}

	method render-identifier(Identifier $ident --> Str) {
		$ident.quoted($!quoting);
	}

	proto method render-expression(Placeholders $placeholders, Expression $expression, Precedence $outer --> Str) {
		my $result = {*};
		parenthesize-if($result, $outer > $expression.precedence);
	}
	multi method render-expression(Placeholders $placeholders, Query::Proxy $proxy, Precedence $precedence --> Str) {
		self.render-query($placeholders, $proxy.query, $precedence);
	}
	multi method render-expression(Placeholders $placeholders, Literal $literal, Precedence --> Str) {
		$literal.payload.subst(/ <!after '\\'> '?'/, { $placeholders.bind($literal.arguments.shift) }, :g);
	}
	multi method render-expression(Placeholders $placeholders, Placeholder $placeholder, Precedence --> Str) {
		$placeholders.bind($placeholder.value);
	}
	multi method render-expression(Placeholders $placeholders, Integer $integer, Precedence --> Str) {
		~$integer.value;
	}
	multi method render-expression(Placeholders $placeholders, String $string, Precedence --> Str) {
		"'" ~ $string.value.subst("'", "''", :g) ~ "'";
	}
	multi method render-expression(Placeholders $placeholders, Constant $constant, Precedence --> Str) {
		~$constant;
	}
	multi method render-expression(Placeholders $placeholders, Value::List $parentheses, Precedence --> Str) {
		self.render-expression-list($placeholders, $parentheses.elements, False);
	}
	multi method render-expression(Placeholders $placeholders, Op::Cast $cast, Precedence $ --> Str) {
		"CAST({ self.render-expression($placeholders, $cast.primary, Precedence::Comma) } AS $cast.typename())";
	}
	method render-when(Placeholders $placeholders, Op::Case::When $when --> List) {
		my $condition = self.render-expression($placeholders, $when.condition, Precedence::Comma);
		my $then      = self.render-expression($placeholders, $when.then, Precedence::Comma);
		'WHEN', $condition, 'THEN', $then;
	}
	multi method render-expression(Placeholders $placeholders, Op::Case $case, Precedence $ --> Str) {
		my @result = 'CASE';
		@result.append: self.render-expression($placeholders, $case.left, Precedence::Comma) with $case.left;
		@result.append: $case.whens.map: { self.render-when($placeholders, $^when) };
		@result.append: 'ELSE', self.render-expression($placeholders, $case.else, Precedence::Comma) with $case.else;
		@result.append: 'END';
		@result.join(' ');
	}
	multi method render-expression(Placeholders $placeholders, Op::Prefix $op, Precedence --> Str) {
		my $primary = self.render-expression($placeholders, $op.value, Precedence::Prefix);
		$op.operator ~ $primary;
	}
	multi method render-expression(Placeholders $placeholders, Op::Binary $op, Precedence --> Str) {
		my $left = self.render-expression($placeholders, $op.left, $op.precedence);
		my $right = self.render-expression($placeholders, $op.right, $op.precedence);
		"$left $op.operator() $right";
	}
	multi method render-expression(Placeholders $placeholders, Op::Liking $liking, Precedence --> Str) {
		my @result;
		@result.push: self.render-expression($placeholders, $liking.left, $liking.precedence);
		@result.push: $liking.negated ?? 'NOT LIKE' !! 'LIKE';
		@result.push: self.render-expression($placeholders, $liking.right, $liking.precedence);
		@result.push: 'ESCAPE', self.render-expression($placeholders, $liking.escape, $liking.precedence) with $liking.escape;
		@result.join(' ');
	}
	multi method render-expression(Placeholders $placeholders, Op::Postfix $op, Precedence --> Str) {
		my $value = self.render-expression($placeholders, $op.left, Precedence::Postfix);
		"$value $op.postfix()";
	}
	multi method render-expression(Placeholders $placeholders, Op::Not $op, Precedence --> Str) {
		my $primary = self.render-expression($placeholders, $op.value, Precedence::Not);
		"NOT $primary";
	}
	multi method render-expression(Placeholders $placeholders, Op::In::List $in, Precedence --> Str) {
		my $left = self.render-expression($placeholders, $in.left, $in.precedence);
		my $candidates = self.render-expression($placeholders, $in.values, Precedence::Comma);
		my $operator = $in.negated ?? 'NOT IN' !! 'IN';
		"$left $operator $candidates";
	}
	multi method render-expression(Placeholders $placeholders, Op::Subquery $subquery, Precedence --> Str) {
		my $left = self.render-expression($placeholders, $subquery.left, $subquery.precedence);
		my $candidates = self.render-expression($placeholders, $subquery.query, Precedence::Comma);
		my $operator = $subquery.operator;
		"$left $operator $candidates";
	}
	multi method render-expression(Placeholders $placeholders, Op::Exists $in, Precedence --> Str) {
		my $candidates = self.render-expression($placeholders, $in.query, Precedence::Comma);
		my $operator = $in.negated ?? 'NOT EXISTS' !! 'EXISTS';
		"$operator $candidates";
	}
	multi method render-expression(Placeholders $placeholders, Op::BetweenLike $between, Precedence --> Str) {
		my $left = self.render-expression($placeholders, $between.left, Precedence::Between);
		my $operator = $between.negated ?? 'NOT BETWEEN' !! 'BETWEEN';
		my $min = self.render-expression($placeholders, $between.min, Precedence::Between);
		my $max = self.render-expression($placeholders, $between.max, Precedence::Between);
		"$left $operator $min AND $max";
	}
	multi method render-expression(Placeholders $placeholders, Op::Logical $logical, Precedence --> Str) {
		self.render-expressions($placeholders, $logical.elements, $logical.precedence, " $logical.operator() ");
	}
	multi method render-expression(Placeholders $placeholders, Identifier $column, Precedence --> Str) {
		self.render-identifier($column);
	}
	multi method render-expression(Placeholders $placeholders, Expression::Renamed $column, Precedence --> Str) {
		my $input = self.render-expression($placeholders, $column.source, Precedence::Comma);
		my $output = self.render-identifier($column.alias);
		"$input AS $output";
	}
	multi method render-expression(Placeholders $placeholders, Current $current, Precedence --> Str) {
		my $cursor = self.render-identifier($current.cursor);
		"CURRENT OF $cursor";
	}
	multi method render-expression(Placeholders $placeholders, Expression::Custom $custom, Precedence --> Str) {
		$custom.render-sql(self);
	}

	multi method render-expression(Placeholders $placeholders, Function $function, Precedence --> Str) {
		my @expressions = $function.quantifier ?? $function.quantifier.uc !! Empty;
		@expressions.append: self.render-column-expressions($placeholders, $function.arguments);
		@expressions.append: self.render-order-by($placeholders, $function.order-by);
		my $expression = @expressions.join(' ');
		my @result = "{ $function.function() }($expression)";
		@result.push: 'FILTER', parenthesize-list(self.render-conditions($placeholders, $function.filter, 'WHERE')) with $function.filter;
		@result.join(' ');
	}

	multi method render-window-boundary(Placeholders $placeholders, Window::Boundary::Unbounded $boundary) {
		$boundary.tag;
	}
	multi method render-window-boundary(Placeholders $placeholders, Window::Boundary::Bounded $boundary) {
		self.render-expression($placeholders, $boundary.offset, Precedence::Between), $boundary.tag;
	}

	multi method render-window-frame(Placeholders $placeholders, Window::Frame $frame) {
		my $mode = $frame.mode.uc;
		my @exclusion = $frame.exclude.defined ?? ('EXCLUDE', $frame.exclude.uc) !! Empty;
		my $from = self.render-window-boundary($placeholders, $frame.from);
		$mode, $from, |@exclusion;
	}
	multi method render-window-frame(Placeholders $placeholders, Window::Frame $frame where .to.defined) {
		my $mode = $frame.mode.uc;
		my $from = self.render-window-boundary($placeholders, $frame.from);
		my $to = self.render-window-boundary($placeholders, $frame.to);
		my @exclusion = $frame.exclude.defined ?? ('EXCLUDE', $frame.exclude.uc) !! Empty;
		$mode, 'BETWEEN', $from, 'AND', $to, |@exclusion;
	}

	multi method render-window-definition(Placeholders $placeholders, Window::Definition $definition --> Str) {
		my @inner;
		@inner.push: self.render-identifier($definition.existing) with $definition.existing;
		@inner.push: self.render-column-expressions($placeholders, $definition.partition-by, 'PARTITION BY');
