Red.git | lib/Red/Driver/ | Pg.rakumod
use DB::Pg;
use Red::Driver;
use Red::Driver::CommonSQL;
use Red::Statement;
use Red::AST::Unary;
use Red::AST::Infixes;
use Red::AST::Value;
use X::Red::Exceptions;
use Red::AST::TableComment;
use Red::Type::Json;
use Red::LockType;
need UUID;
unit class Red::Driver::Pg does Red::Driver::CommonSQL;
has Str $!user;
has Str $!password;
has Str $!host;
has Int $!port;
has Str $!dbname;
has $.dbh;
method schema-reader {}
# Data accepted by the Pg driver constructor:
# dbh : DB::Pg object
# user : User to be used to connect to the database
# password: Password to be used to connect to the database
# host : To be connected to
# port : What port to connect
# dbname : Database name
submethod BUILD(:$!dbh, Str :$!user, Str :$!password, Str :$!host = "127.0.0.1", Int :$!port = 5432, Str :$!dbname) {}
submethod TWEAK() {
$!dbh //= DB::Pg.new:
conninfo => "{
"user=$_" with $!user
} {
"password=$_" with $!password
} {
"host=$_" with $!host
} {
"port=$_" with $!port
} {
"dbname=$_" with $!dbname
}"
;
}
# JSON values are stringified when binding/reading
method stringify-json { True }
method new-connection($dbh = $!dbh) { self.clone: dbh => $dbh }
method begin {
my $dbh = $!dbh.db;
$dbh.begin;
self.new-connection: $dbh
}
method commit { $!dbh.commit.finish }
method rollback { $!dbh.rollback }
multi method translate(Red::AST::DateTimePart $_, $context?) {
my ($sql, @bind) = do given self.translate: .base, $context { .key, |.value }
"EXTRACT({ .part.key.uc } FROM { $sql })" => @bind
}
multi method agg-prefetch($_) {
qq:to/END/;
json_agg(json_build_object({
.^columns.map({
"'{ .column.attr-name }', { self.table-name-wrapper: .package.^table }.{ .column.name }"
}).join: ", "
})) as json
END
}
# Postgres uses $1, $2 ... style placeholders
method wildcard { '$' ~ ++$*bind-counter }
multi method translate(Red::AST::Not $_ where .value ~~ Red::Column, $context?) {
self.translate: Red::AST::Cast.new(.value, "boolean").not
}
multi method translate(Red::AST::AND $_ where .left ~~ Red::Column, $context?) {
self.translate: Red::AST::AND.new: Red::AST::Cast.new(.left, "boolean"), .right
}
multi method translate(Red::AST::AND $_ where .right ~~ Red::Column, $context?) {
self.translate: Red::AST::AND.new: .left, Red::AST::Cast.new(.right, "boolean")
}
multi method translate(Red::AST::OR $_ where .left ~~ Red::Column, $context?) {
self.translate: Red::AST::OR.new: Red::AST::Cast.new(.left, "boolean"), .right
}
multi method translate(Red::AST::OR $_ where .right ~~ Red::Column, $context?) {
self.translate: Red::AST::OR.new: .left, Red::AST::Cast.new(.right, "boolean")
}
multi method translate(Red::AST::Cast $_ where { .type eq "boolean" && .value.?returns ~~ DateTime }, $context?) {
self.translate: Red::AST::IsDefined.new: .value;
}
multi method translate(Red::AST::Cast $_, $context?) {
my &trans = sub ($_) {
when "str" { "TEXT" }
when "int" { "INTEGER" }
when "num" { "NUMERIC" }
when "datetime" { "TIMESTAMP" }
when "date" { "DATE" }
when "json" { "JSONB" }
when "uuid" { "UUID" }
when "blob" { "BYTEA" }
default { $_ }
}
when Red::AST::Value {
.bind ?? self.translate(.value, "bind") !! qq|'{ .value }'| => []
}
default {
my ($str, @bind) := do given self.translate: .value, .bind ?? "bind" !! $context { .key, .value }
"({ $str })::{ .type.&trans }" => @bind
}
}
# Translate string functions that differ on Pg
multi method translate(Red::AST::Index $_, $context?) {
my ($base, @bb) := do given self.translate: .base, $context { .key, .value };
my $needle = do given .needle {
when Red::AST { $_ }
default { ast-value $_ }
};
my ($needles, @nb) := do given self.translate: $needle, $context { .key, .value };
"STRPOS($base, $needles)" => [|@bb, |@nb]
}
# MINUS is EXCEPT in Postgres
multi method translate(Red::AST::Minus $ast, "multi-select-op") { "EXCEPT" => [] }
# JSONB support
proto method pg-jsonb-path-item(|) {*}
multi method pg-jsonb-path-item(@items) { @items.map({ self.pg-jsonb-path-item: $_ }).join(',') }
multi method pg-jsonb-path-item(Red::AST::Value $_) { self.pg-jsonb-path-item: .value }
multi method pg-jsonb-path-item(Int $_) { $_ }
multi method pg-jsonb-path-item(Str $_) { $_ }
multi method translate(Red::AST::JsonItem $_, $context?) {
my ($left, @lb) := do given self.translate: .left, $context { .key, .value };
my $path = self.pg-jsonb-path-item: .right.value;
"($left #> '{ $path }')" => @lb
}
multi method translate(Red::AST::JsonRemoveItem $_, $context?) {
my ($left, @lb) := do given self.translate: .left, $context { .key, .value };
my $path = self.pg-jsonb-path-item: .right.value;
"($left #- '{ $path }')" => @lb
}
multi method translate(Red::AST::Value $_ where { .type ~~ Pair and .value.key ~~ Red::AST::JsonItem }, "update") {
my $json-item = .value.key;
my $val = .value.value;
my ($col, @cb) := do given self.translate: $json-item.left, 'update-lval' { .key, .value };
my $path = self.pg-jsonb-path-item: $json-item.right.value;
my $ph = self.wildcard;
my @bind = [ self.wildcard-value: $val ];
my $newval-sql = $val.?returns ~~ Json ?? "($ph)::jsonb" !! "to_jsonb($ph)";
"{ $col } = jsonb_set({ $col }, '{ $path }', { $newval-sql }, true)" => [|@cb, |@bind]
}
multi method translate(Red::Column $_, "column-auto-increment") {}
multi method translate(Red::AST::Select $_, $context?, :$gambi where !*.defined) {
my $bind-counter = $*bind-counter // 0;
{
my Int $*bind-counter = $bind-counter;
self.Red::Driver::CommonSQL::translate($_, $context, :gambi);
}
}
multi method translate(Red::AST::Update $_, $context?, :$gambi where !*.defined) {
my Int $*bind-counter;
my $*red-subselect-for = UPDATE;
self.Red::Driver::CommonSQL::translate($_, $context, :gambi);
}
multi method wildcard-value(@val) { @val.map: { self.wildcard-value: $_ } }
multi method wildcard-value($_ where { .HOW ~~ Metamodel::EnumHOW }) { .value }
multi method wildcard-value(Red::AST::Value $_) { self.wildcard-value: .get-value }
multi method wildcard-value(Bool $_) { .Int }
multi method wildcard-value($_) { $_ }
multi method translate(Red::AST::RowId $_, $context?) { "OID" => [] }
multi method translate($, "delete-returning") { "RETURNING *" => [] }
multi method translate($, "update-returning") { "RETURNING *" => [] }
multi method translate(Red::AST::Delete $_, $context?, :$gambi where !*.defined) {
my Int $*bind-counter;
my $*red-subselect-for = UPDATE;
self.Red::Driver::CommonSQL::translate($_, $context, :gambi);
}
multi method translate(Red::AST::Insert $_, $context?) {
my Int $*bind-counter;
my @values = .values.grep({ .value.value.defined });
return "INSERT INTO { self.table-name-wrapper: .into.^table } DEFAULT VALUES RETURNING *" => [] unless @values;
my @bind = @values.map: { self.wildcard-value: .value };
"INSERT INTO {
self.table-name-wrapper: .into.^table
}(\n{
@values>>.key.join(",\n").indent: 3
}\n)\nVALUES(\n{
(self.wildcard xx @values).join(",\n").indent: 3
}\n) RETURNING *" => @bind
}
multi method translate(Red::AST::Mod $_, $context?) {
my ($ls, @lb) := do given self.translate: .left, $context { .key, .value }
my ($rs, @rb) := do given self.translate: .right, $context { .key, .value }
"mod($ls, $rs)" => [|@lb, |@rb]
}
multi method translate(Red::AST::Value $_ where .type ~~ Bool, $context?) { (.value ?? "'t'" !! "'f'") => [] }
multi method translate(Red::AST::Value $_ where .type ~~ UUID, $context?) { "'{ .value.Str }'" => [] }
multi method translate(Red::AST::Value $_ where .type ~~ Rat, $context?) { "'{ .value }'::{ self.default-type-for-type: .type }" => [] }
multi method translate(Red::Column $_, "column-comment") {
(.comment ?? qq[COMMENT ON COLUMN { self.translate: $_, "table-dot-column" } IS '{ .comment }'] !! "") => []
}
multi method translate(Red::Column $_, "column-type") {
if .attr.type.?red-type-column-type -> $type { return self.type-by-name($type) => [] }
if !.auto-increment && .attr.type =:= Mu && !.type.defined { return self.type-by-name("string") => [] }
(.type.defined ?? self.type-by-name(.type) !! self.default-type-for: $_) => []
}
multi method translate(Red::AST::TableComment $_, $context?) { "COMMENT ON TABLE { .table } IS '{ .msg }'" => [] }
class Statement does Red::Statement {
has Str $.query;
method stt-exec($stt, *@bind) {
$!driver.debug: $!query, @bind || @!binds;
my $db = $stt ~~ DB::Pg ?? $stt.db !! $stt;
my $sth = $db.prepare($!query);
my $s = $sth.execute(|(@bind or @!binds));
$db.finish if $stt ~~ DB::Pg;
do if $s ~~ DB::Pg::Results { $s.hashes } else { [] }.iterator
}
method stt-row($stt) { $stt.pull-one }
}
method comment-on-same-statement { False }
multi method prepare(Str $query) {
CATCH { default { self.map-exception($_).throw } }
Statement.new: :driver(self), :statement($!dbh), :$query
}
multi method default-type-for(Red::Column $ where .auto-increment --> Str:D) { "serial" }
multi method default-type-for-type(Positional $_ --> Str:D) { "{ self.default-type-for-type: .of }[]" }
multi method default-type-for-type(Json --> Str:D) { "jsonb" }
multi method default-type-for-type(DateTime --> Str:D) { "timestamp" }
multi method default-type-for-type(Instant --> Str:D) { "timestamp" }
multi method default-type-for-type(Date --> Str:D) { "date" }
multi method default-type-for-type(Bool --> Str:D) { "boolean" }
multi method default-type-for-type(Int --> Str:D) { "integer" }
multi method default-type-for-type(UUID --> Str:D) { "uuid" }
multi method default-type-for-type(Blob --> Str:D) { "bytea" }
multi method default-type-for-type(Red::Column $ --> Str:D) { "varchar(255)" }
multi method type-for-sql("jsonb" --> "Json") {}
multi method inflate(Str $value, DateTime :$to!) { DateTime.new: $value }
multi method inflate(Blob $value, :$to!) { $to.new: $value }
multi method deflate(Instant:D $value) { ~$value.DateTime.utc }
multi method deflate(DateTime:D $value) { ~$value.utc }
multi method map-exception(DB::Pg::Error::FatalError $x where .?message ~~ /"duplicate key value violates unique constraint " \"$<field>=(\w+)\"/) {
X::Red::Driver::Mapped::Unique.new: :driver<Pg>, :orig-exception($x), :fields($<field>.Str)
}
multi method map-exception(DB::Pg::Error::FatalError $x where /"duplicate key value violates unique constraint"/) {
$x.?message-detail ~~ /"Key (" \s* (\w+)+ % [\s* "," \s*] \s* ")=(" .*? ") already exists."/;
my @fields = $0 ?? $0>>.Str !! "";
X::Red::Driver::Mapped::Unique.new: :driver<Pg>, :orig-exception($x), :@fields,
}
multi method map-exception(DB::Pg::Error::FatalError $x where /"duplicate key value violates unique constraint"/) {
$x.?message ~~ /"DETAIL: Key (" \s* (\w+)+ % [\s* "," \s*] \s* ")=(" .*? ") already exists."/;
X::Red::Driver::Mapped::Unique.new: :driver<Pg>, :orig-exception($x), :fields($0>>.Str)
}
multi method map-exception(DB::Pg::Error::FatalError $x where .?message ~~ /relation \s+ \"$<table>=(\w+)\" \s+ already \s+ exists/) {
X::Red::Driver::Mapped::TableExists.new: :driver<Pg>, :orig-exception($x), :table($<table>.Str)
}