Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F12423897
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
25 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/Engines/PDOEngine.php b/src/Engines/PDOEngine.php
index 5c14926..2705f0e 100644
--- a/src/Engines/PDOEngine.php
+++ b/src/Engines/PDOEngine.php
@@ -1,189 +1,214 @@
<?php
namespace Keruald\Database\Engines;
use Keruald\Database\Database;
use Keruald\Database\DatabaseEngine;
use Keruald\Database\Exceptions\EngineSetupException;
use Keruald\Database\Exceptions\NotImplementedException;
use Keruald\Database\Exceptions\SqlException;
use Keruald\Database\Result\PDODatabaseResult;
+use Keruald\Database\Query\PDOQuery;
use PDO;
use PDOException;
use RuntimeException;
abstract class PDOEngine extends DatabaseEngine {
protected PDO $db;
private int $fetchMode = PDO::FETCH_ASSOC;
protected ?PDOException $lastException = null;
/**
* Sends a unique query to the database.
*
* @param string $query
*
* @return PDODatabaseResult|bool
*/
public function query (string $query) : PDODatabaseResult|bool {
try {
$result = $this->db->query($query);
} catch (PDOException $ex) {
if ($this->dontThrowExceptions) {
return false;
}
$this->lastException = $ex;
$this->onQueryError($query);
}
return new PDODatabaseResult($result, $this->fetchMode);
}
public function nextId () : int|string {
return $this->db->lastInsertId();
}
+ public function setLastException (PDOException $ex) : void {
+ $this->lastException = $ex;
+ }
+
protected function getExceptionContext () : array {
$info = match ($this->lastException) {
null => $this->db->errorInfo(),
default => $this->lastException->errorInfo,
};
return self::parseErrorInfo($info);
}
private static function parseErrorInfo(array $info) : array {
$context = [];
// SQLSTATE error code
if ((int)$info[0] > 0) {
$context["errno"] = $info[0];
}
// Driver-specific error message
if ($info[2] !== null) {
$context["error"] = $info[2];
}
return $context;
}
public static function load (array $config) : PDOEngine {
$config = self::getConfig($config);
try {
$instance = new static(
$config['host'],
$config['username'],
$config['password'],
$config['database'],
);
} catch (RuntimeException $ex) {
throw new EngineSetupException(
$ex->getMessage(),
$ex->getCode(),
$ex
);
}
// Extra configuration
$instance->fetchMode = (int)$config["fetch_mode"];
return $instance;
}
public static function initialize (array &$config) : PDOEngine {
$instance = Database::initialize($config);
if ($instance instanceof PDOEngine) {
return $instance;
}
throw new EngineSetupException(
"Invalid database engine for PDO: " . get_class($instance)
);
}
private static function getConfig (array $config) : array {
return $config + [
'host' => 'localhost',
'username' => '',
'password' => '',
'database' => '',
'fetch_mode' => PDO::FETCH_ASSOC,
];
}
public function getUnderlyingDriver () : PDO {
return $this->db;
}
public function error () : array {
return self::parseErrorInfo($this->db->errorInfo());
}
///
/// Events
///
protected function onCantConnectToHost () : void {
$previous = $this->lastException;
$code = match($previous) {
null => 0,
default => $previous->getCode(),
};
$appendToMessage = match($previous) {
null => "",
default => ": " . $previous->getMessage(),
};
$ex = new RuntimeException(
"Can't connect to SQL server" . $appendToMessage,
$code,
$previous,
);
if (!class_exists(self::EVENTS_PROPAGATION_CLASS)) {
throw $ex;
}
$callable = [self::EVENTS_PROPAGATION_CLASS, "callOrThrow"];
$callable($this->cantConnectToHostEvents, [$this, $ex], $ex);
}
- protected function onQueryError (string $query) : void {
+ public function onQueryError (string $query) : void {
$ex = SqlException::fromQuery(
$query,
$this->getExceptionContext(),
);
if (!class_exists(self::EVENTS_PROPAGATION_CLASS)) {
throw $ex;
}
$callable = [self::EVENTS_PROPAGATION_CLASS, "callOrThrow"];
$callable($this->queryErrorEvents, [$this, $query, $ex], $ex);
}
+ ///
+ /// PDO features
+ ///
+
+ public abstract function hasInOutSupport() : bool;
+
+ /**
+ * Prepares a query for later execution
+ *
+ * @param string $query
+ * @param int[] $options
+ * @return PDOQuery
+ */
+ public function prepare (string $query, array $options = []) : PDOQuery {
+ $statement = $this->db->prepare($query, $options);
+
+ return PDOQuery::from($this, $statement)
+ ->withFetchMode($this->fetchMode);
+ }
+
///
/// Not implemented features
///
/**
* @throws NotImplementedException
*/
public function countAffectedRows () : int {
throw new NotImplementedException(<<<END
With PDO drivers, you can get the number of affected rows
for any SQL query using PDODatabaseResult::numRows().
END);
}
}
diff --git a/src/Engines/PgsqlPDOEngine.php b/src/Engines/PgsqlPDOEngine.php
index 861f933..1ff45bd 100644
--- a/src/Engines/PgsqlPDOEngine.php
+++ b/src/Engines/PgsqlPDOEngine.php
@@ -1,63 +1,67 @@
<?php
namespace Keruald\Database\Engines;
use Keruald\Database\Exceptions\NotImplementedException;
use PDO;
use Pdo\Pgsql;
use RuntimeException;
final class PgsqlPDOEngine extends PDOEngine {
use WithPDOPostgreSQL;
const string PDO_CLASS = Pgsql::class;
///
/// Constructor
///
public function __construct (
string $host = 'localhost',
string $username = 'root',
string $password = '',
string $database = ''
) {
// Checks extension requirement
if (!class_exists(self::PDO_CLASS)) {
throw new RuntimeException("This engine requires PHP 8.4+ Pdo\Pgsql PostgreSQL PDO driver.");
}
// Connects to PostgreSQL server
$dsn = "pgsql:host=$host";
if ($database !== "") {
$dsn .= ";dbname=$database";
}
$this->db = new Pgsql($dsn, $username, $password);
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
///
/// Not implemented features
///
/**
* @throws NotImplementedException
*/
public function escape (string $expression) : string {
throw new NotImplementedException(<<<END
This PDO engine does not support escape for literals.
Placeholders are recommended instead for PDO operations.
END);
}
+ public function hasInOutSupport() : bool {
+ return false;
+ }
+
///
/// Engine-specific methods
///
public function escapeIdentifier (string $expression) : string {
return $this->db->escapeIdentifier($expression);
}
}
diff --git a/src/Engines/PostgreSQLPDOEngine.php b/src/Engines/PostgreSQLPDOEngine.php
index 4e21b42..9eeb6c1 100644
--- a/src/Engines/PostgreSQLPDOEngine.php
+++ b/src/Engines/PostgreSQLPDOEngine.php
@@ -1,60 +1,64 @@
<?php
namespace Keruald\Database\Engines;
use Keruald\Database\Exceptions\NotImplementedException;
use PDO;
use PDOException;
use RuntimeException;
class PostgreSQLPDOEngine extends PDOEngine {
use WithPDOPostgreSQL;
const string PDO_CLASS = PDO::class;
public function __construct (
string $host = 'localhost',
string $username = 'root',
string $password = '',
string $database = ''
) {
// Checks extension requirement
if (!class_exists(self::PDO_CLASS)) {
throw new RuntimeException("This engine requires PDO extension.");
}
// Connects to PostgreSQL server
$dsn = "pgsql:host=$host";
if ($database !== "") {
$dsn .= ";dbname=$database";
}
try {
$this->db = new PDO($dsn, $username, $password);
} catch (PDOException $ex) {
$this->lastException = $ex;
$this->onCantConnectToHost();
return;
}
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
///
/// Not implemented features
///
/**
* @throws NotImplementedException
*/
public function escape (string $expression) : string {
throw new NotImplementedException(<<<END
This PDO engine does not support escape for literals.
Placeholders are recommended instead for PDO operations.
END);
}
+ public function hasInOutSupport() : bool {
+ return false;
+ }
+
}
diff --git a/src/Query/DatabaseQuery.php b/src/Query/DatabaseQuery.php
new file mode 100644
index 0000000..3be09bc
--- /dev/null
+++ b/src/Query/DatabaseQuery.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace Keruald\Database\Query;
+
+use Keruald\Database\Result\DatabaseResult;
+
+abstract class DatabaseQuery {
+
+ public abstract function query() : ?DatabaseResult;
+
+ public abstract function __toString() : string;
+
+}
diff --git a/src/Query/PDOQuery.php b/src/Query/PDOQuery.php
new file mode 100644
index 0000000..6cde7fc
--- /dev/null
+++ b/src/Query/PDOQuery.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace Keruald\Database\Query;
+
+use Keruald\Database\Engines\PDOEngine;
+use Keruald\Database\Exceptions\NotImplementedException;
+use Keruald\Database\Result\PDODatabaseResult;
+
+use PDO;
+use PDOException;
+use PDOStatement;
+
+class PDOQuery extends DatabaseQuery {
+
+ ///
+ /// Private members
+ ///
+
+ private PDOEngine $db;
+
+ private PDOStatement $statement;
+
+ private int $fetchMode = PDO::FETCH_ASSOC;
+
+ private bool $isInOutDefined = false;
+
+ ///
+ /// Constructors
+ ///
+
+ public function __construct (PDOEngine $db, PDOStatement $statement) {
+ $this->db = $db;
+ $this->statement = $statement;
+ }
+
+ public static function from (PDOEngine $db, PDOStatement $statement) : self {
+ return new self($db, $statement);
+ }
+
+ ///
+ /// Getters and setters
+ ///
+
+ public function getFetchMode () : int {
+ return $this->fetchMode;
+ }
+
+ public function setFetchMode (int $mode) : void {
+ $this->fetchMode = $mode;
+ }
+
+ public function withFetchMode (int $mode) : self {
+ $this->fetchMode = $mode;
+
+ return $this;
+ }
+
+ ///
+ /// PDO statements like interaction
+ ///
+
+ public function query() : ?PDODatabaseResult {
+ if ($this->isInOutDefined && !$this->db->hasInOutSupport()) {
+ throw new NotImplementedException("InOut parameters are not supported by this engine.");
+ }
+
+ try {
+ $result = $this->statement->execute();
+ } catch (PDOException $ex) {
+ if ($this->db->dontThrowExceptions) {
+ return null;
+ }
+
+ $this->db->setLastException($ex);
+ $this->db->onQueryError($this->statement->queryString);
+ }
+
+ return new PDODatabaseResult($this->statement, $this->fetchMode);
+ }
+
+ public function with (int|string $name, mixed $value, ?int $type = null) : self {
+ $type = $type ?? self::resolveParameterType($value);
+ $this->statement->bindValue($name, $value, $type);
+
+ return $this;
+ }
+
+ public function withIndexedValue(int $position, mixed $value, ?int $type = null) : self {
+ $type = $type ?? self::resolveParameterType($value);
+ $this->statement->bindValue($position, $value, $type);
+
+ return $this;
+ }
+
+ public function withValue(string $name, mixed $value, ?int $type = null) : self {
+ $type = $type ?? self::resolveParameterType($value);
+ $this->statement->bindValue($name, $value, $type);
+
+ return $this;
+ }
+
+ public function bind(string $name, mixed &$value, ?int $type = null) : self {
+ $type = $type ?? self::resolveParameterType($value);
+ $this->statement->bindParam($name, $value, $type);
+
+ return $this;
+ }
+
+ public function bindInOutParameter(string $name, mixed &$value, ?int $type = null) : self {
+ $type = $type ?? self::resolveParameterType($value);
+ $this->statement->bindParam($name, $value, $type | PDO::PARAM_INPUT_OUTPUT);
+
+ $this->isInOutDefined = true;
+
+ return $this;
+ }
+
+ ///
+ /// PDO_PARAM_* type resolution
+ ///
+
+ public static function resolveParameterType(mixed $value) : int {
+ if (is_int($value)) {
+ return PDO::PARAM_INT;
+ }
+
+ if (is_null($value)) {
+ return PDO::PARAM_NULL;
+ }
+
+ if (is_bool($value)) {
+ return PDO::PARAM_BOOL;
+ }
+
+ return PDO::PARAM_STR;
+ }
+
+ ///
+ /// Low-level interactions
+ ///
+
+ public function getUnderlyingStatement () : PDOStatement {
+ return $this->statement;
+ }
+
+ ///
+ /// Implements Stringable
+ ///
+
+ public function __toString () : string {
+ return $this->statement->queryString;
+ }
+
+}
diff --git a/tests/Engines/BasePDOPostgreSQLTestCase.php b/tests/Engines/BasePDOPostgreSQLTestCase.php
index aa5e527..c8ec7d7 100644
--- a/tests/Engines/BasePDOPostgreSQLTestCase.php
+++ b/tests/Engines/BasePDOPostgreSQLTestCase.php
@@ -1,94 +1,107 @@
<?php
namespace Keruald\Database\Tests\Engines;
use Keruald\Database\Exceptions\NotImplementedException;
use Keruald\Database\Exceptions\SqlException;
use Keruald\Database\Result\PDODatabaseResult;
+use PDO;
+
abstract class BasePDOPostgreSQLTestCase extends BasePDOTestCase {
const string DB_NAME = "test_keruald_db";
public function testQueryScalarWithNonSelectQuery(): void {
$this->expectException(\LogicException::class);
$sql = "UPDATE numbers SET number = number * 2";
$this->db->queryScalar($sql);
}
public function testFetchRow(): void {
$sql = "SELECT 10 UNION SELECT 20 UNION SELECT 30 ORDER BY 1";
$result = $this->db->query($sql);
// PostgreSQL uses "?column?" as default column name for unnamed columns
$this->assertEquals(10, $result->fetchRow()['?column?']);
$this->assertEquals(20, $result->fetchRow()['?column?']);
$this->assertEquals(30, $result->fetchRow()['?column?']);
// Then, we get a null value
$this->assertEquals(null, $result->fetchRow());
}
public function testArrayShapeForFetchRow(): void {
$sql = "SELECT 10 as score, 50 as \"limit\"";
$result = $this->db->query($sql);
$expected = [
// By column name
"score" => 10,
"limit" => 50
];
$this->assertEquals($expected, $result->fetchRow());
}
public function testQueryWhenItSucceeds(): void {
$result = $this->db->query("DELETE FROM numbers");
$this->assertInstanceOf(PDODatabaseResult::class, $result);
}
public function testQueryWhenItFailsWithException(): void {
$this->expectException(SqlException::class);
$this->db->query("TRUNCATE not_existing_table");
}
public function testQueryWithWrongQueryInLegacyMode(): void {
$this->db->dontThrowExceptions = true;
$result = $this->db->query("TRUNCATE not_existing");
$this->assertFalse($result);
}
public function testNextId(): void {
// Use a transaction to isolate this test
// Arcanist creates a race condition on this when running from `arc diff`
$this->db->query("BEGIN");
try {
// PostgreSQL uses sequences for auto-increment
$this->db->query("TRUNCATE numbers RESTART IDENTITY");
$this->db->query("INSERT INTO numbers (id, number) VALUES (1700, 42742)");
$this->db->query("INSERT INTO numbers (number) VALUES (666)");
// Get the last inserted ID
$lastId = $this->db->nextId();
$this->assertGreaterThan(0, $lastId);
$this->db->query("ROLLBACK"); // No need to commit, tests done
} catch (\Exception $ex) {
$this->db->query("ROLLBACK");
$this->fail($ex->getMessage());
}
}
public function testEscapeNotImplemented(): void {
$this->expectException(NotImplementedException::class);
$this->expectExceptionMessage('This PDO engine does not support escape for literals');
$this->db->escape("test'string");
}
+
+ public function testInOut () : void {
+ $this->expectException(NotImplementedException::class);
+
+ $port = 8000;
+ $query = $this->db
+ ->prepare("CALL define_port(:port);")
+ ->bindInOutParameter("port", $port, PDO::PARAM_INT)
+ ->query();
+ }
+
}
diff --git a/tests/Engines/BasePDOTestCase.php b/tests/Engines/BasePDOTestCase.php
index 5ac5f1c..ff3bc59 100644
--- a/tests/Engines/BasePDOTestCase.php
+++ b/tests/Engines/BasePDOTestCase.php
@@ -1,35 +1,184 @@
<?php
namespace Keruald\Database\Tests\Engines;
use Keruald\Database\Engines\PDOEngine;
-
use Keruald\Database\Exceptions\SqlException;
+use Keruald\Database\Query\PDOQuery;
+use Keruald\Database\Result\PDODatabaseResult;
+
+use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
+use PDO;
+use PDOStatement;
+
abstract class BasePDOTestCase extends TestCase {
protected PDOEngine $db;
protected static abstract function buildEngine () : PDOEngine;
protected function setUp (): void {
$this->db = static::buildEngine();
}
public function testQueryScalar(): void {
$sql = "SELECT 1+1";
$this->assertEquals(2, $this->db->queryScalar($sql));
}
public function testQueryScalarWithoutQuery(): void {
$this->assertEquals("", $this->db->queryScalar(""));
}
public function testQueryScalarWithWrongQuery(): void {
$this->expectException(SqlException::class);
$sql = "DELETE FROM nonexisting";
$this->db->queryScalar($sql);
}
+
+ ///
+ /// Integration tests for PDOQuery
+ ///
+
+ public function testPrepare(): void {
+ $sql = "SELECT :word";
+
+ $query = $this->db->prepare($sql);
+ $this->assertInstanceOf(PDOQuery::class, $query);
+
+ $result = $query
+ ->with("word", "foo")
+ ->query();
+ $this->assertInstanceOf(PDODatabaseResult::class, $result);
+
+ $row = $result->fetchRow();
+ $this->assertContains("foo", $row);
+ }
+
+ public function testWithValue(): void {
+ $query = $this->db
+ ->prepare("SELECT :word")
+ ->withvalue("word", "foo");
+
+ $this->assertEquals("foo", $query->query()->fetchScalar());
+
+ }
+
+ public function testWithValueWithVariableChange(): void {
+ $word = "foo";
+
+ $query = $this->db
+ ->prepare("SELECT :word")
+ ->withvalue("word", $word);
+
+ $word = "bar";
+
+ $this->assertEquals("foo", $query->query()->fetchScalar());
+
+ }
+
+ public function testWithIndexedValue(): void {
+ $query = $this->db
+ ->prepare("SELECT ?")
+ ->withIndexedValue(1, "foo");
+
+ $this->assertEquals("foo", $query->query()->fetchScalar());
+ }
+
+ public function testBind(): void {
+ $word = "foo";
+
+ $query = $this->db
+ ->prepare("SELECT :word")
+ ->bind("word", $word);
+
+ $this->assertEquals("foo", $query->query()->fetchScalar());
+
+ }
+
+ public function testBindWithVariableChange(): void {
+ $word = "foo";
+
+ $query = $this->db
+ ->prepare("SELECT :word")
+ ->bind("word", $word);
+
+ $word = "bar";
+
+ $this->assertEquals("bar", $query->query()->fetchScalar());
+ }
+
+
+ public static function provideFetchModeAndScalarResults(): iterable {
+ yield "PDO::FETCH_ASSOC" => [
+ PDO::FETCH_ASSOC,
+ ["?column?" => "foo"],
+ ];
+
+ yield "PDO::FETCH_NUM" => [
+ PDO::FETCH_NUM,
+ [0 => "foo"],
+ ];
+
+ yield "PDO::FETCH_BOTH" => [
+ PDO::FETCH_BOTH,
+ [0 => "foo", "?column?" => "foo"],
+ ];
+ }
+
+ #[DataProvider("provideFetchModeAndScalarResults")]
+ public function testFetchMode($mode, $expected): void {
+ $actual = $this->db
+ ->prepare("SELECT :word")
+ ->with("word", "foo")
+ ->withFetchMode($mode)
+ ->query()
+ ->fetchRow();
+
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testGetUnderlyingStatement () : void {
+ $query = $this->db
+ ->prepare("SELECT :word");
+
+ $this->assertInstanceOf(PDOStatement::class, $query->getUnderlyingStatement());
+ }
+
+ public function testToString () : void {
+ $query = $this->db
+ ->prepare("SELECT :word");
+
+ $this->assertEquals("SELECT :word", (string)$query);
+ }
+
+ public function testToStringIsInvariant () : void {
+ $query = $this->db
+ ->prepare("SELECT :word")
+ ->with("word", "foo");
+
+ $this->assertEquals("SELECT :word", (string)$query);
+ }
+
+ public function testQueryWithError () : void {
+ $this->expectException(SqlException::class);
+
+ $sql = "SELECT * FROM nonexisting";
+ $result = $this->db->prepare($sql)->query();
+
+ $this->assertNull($result);
+ }
+
+ public function testQueryWithErrorWhenExceptionsAreDisabled () : void {
+ $this->db->dontThrowExceptions = true;
+
+ $sql = "SELECT * FROM nonexisting";
+ $result = $this->db->prepare($sql)->query();
+
+ $this->assertNull($result);
+ }
+
}
diff --git a/tests/Query/PDOQueryTest.php b/tests/Query/PDOQueryTest.php
new file mode 100644
index 0000000..e7a99d0
--- /dev/null
+++ b/tests/Query/PDOQueryTest.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Keruald\Database\Tests\Query;
+
+use Keruald\Database\Engines\PDOEngine;
+use Keruald\Database\Query\PDOQuery;
+
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\TestCase;
+
+use PDO;
+use PDOStatement;
+
+class PDOQueryTest extends TestCase {
+
+ private PDOQuery $query;
+
+ ///
+ /// Tests set up
+ ///
+
+ protected function setUp() : void {
+ $this->query = $this->mockQuery();
+ }
+
+ protected function mockQuery () : PDOQuery {
+ $engine = $this->createMock(PDOEngine::class);
+ $statement = $this->createMock(PDOStatement::class);
+
+ return new PDOQuery($engine, $statement);
+ }
+
+ ///
+ /// Getters and setters
+ ///
+
+ public function testGetAndSetFetchMode () : void {
+ $this->query->setFetchMode(PDO::FETCH_ASSOC);
+ $this->assertEquals(PDO::FETCH_ASSOC, $this->query->getFetchMode());
+ }
+
+ ///
+ /// Static methods
+ ///
+
+ public static function provideParameterTypes () : iterable {
+ yield "int" => [ PDO::PARAM_INT, 1 ];
+ yield "falsy int" => [ PDO::PARAM_INT, 0 ];
+ yield "negative int" => [ PDO::PARAM_INT, -1 ];
+
+ yield "bool" => [ PDO::PARAM_BOOL, true ];
+ yield "falsy bool" => [ PDO::PARAM_BOOL, false ];
+
+ yield "null" => [ PDO::PARAM_NULL, null ];
+
+ yield "string" => [ PDO::PARAM_STR, "foo" ];
+ yield "empty string" => [ PDO::PARAM_STR, "" ];
+ yield "zero string" => [ PDO::PARAM_STR, "0" ];
+
+ // Anything else should also be treated as a string
+ yield "float" => [ PDO::PARAM_STR, 1.0 ];
+ yield "zero float" => [ PDO::PARAM_STR, 0.0 ];
+ }
+
+ #[DataProvider("provideParameterTypes")]
+ public function testResolveParameterType ($type, $value) {
+ $this->assertEquals($type, PDOQuery::resolveParameterType($value));
+ }
+
+}
diff --git a/tests/data/postgresql.sql b/tests/data/postgresql.sql
index cf00b8b..dbbf203 100644
--- a/tests/data/postgresql.sql
+++ b/tests/data/postgresql.sql
@@ -1,27 +1,35 @@
DROP TABLE IF EXISTS numbers CASCADE;
CREATE TABLE numbers
(
id SERIAL PRIMARY KEY,
number INTEGER NULL
);
DROP TABLE IF EXISTS ships CASCADE;
CREATE TABLE ships
(
id SERIAL PRIMARY KEY,
name VARCHAR(255) NULL,
category VARCHAR(3) NULL
);
INSERT INTO ships VALUES
(1, 'So Much For Subtlety', 'GSV'),
(2, 'Unfortunate Conflict Of Evidence', 'GSV'),
(3, 'Just Read The Instructions', 'GCU'),
(4, 'Just Another Victim Of The Ambient Morality', 'GCU');
DROP VIEW IF EXISTS ships_count CASCADE;
CREATE VIEW ships_count AS
SELECT category,
COUNT(category) AS "count(category)"
FROM ships
GROUP BY category;
+
+CREATE OR REPLACE PROCEDURE define_port (OUT Pout INTEGER)
+ LANGUAGE plpgsql
+AS $$
+BEGIN
+ Pout := 1912;
+END;
+$$;
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Nov 6, 19:22 (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3129097
Default Alt Text
(25 KB)
Attached To
Mode
rKDB Keruald Database
Attached
Detach File
Event Timeline
Log In to Comment