diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..27b765f
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+/tests export-ignore
+/.github export-ignore
diff --git a/.github/workflows/close-pull-request.yml b/.github/workflows/close-pull-request.yml
new file mode 100644
index 0000000..07d8be8
--- /dev/null
+++ b/.github/workflows/close-pull-request.yml
@@ -0,0 +1,13 @@
+name: Close Pull Request
+
+on:
+ pull_request_target:
+ types: [ opened ]
+
+jobs:
+ run:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: superbrothers/close-pull-request@v3
+ with:
+ comment: "Hi, this is a READ-ONLY repository, please submit your PR on the https://github.com/hyperf/hyperf repository.
This Pull Request will close automatically.
Thanks! "
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..4ebb5d0
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,25 @@
+on:
+ push:
+ # Sequence of patterns matched against refs/tags
+ tags:
+ - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
+
+name: Release
+
+jobs:
+ release:
+ name: Release
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - name: Create Release
+ id: create_release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ github.ref }}
+ release_name: Release ${{ github.ref }}
+ draft: false
+ prerelease: false
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c35d3f5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) Hyperf
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..8d35a13
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,48 @@
+{
+ "name": "hyperf/database-sqlite",
+ "type": "library",
+ "description": "The sqlite driver for hyperf/database.",
+ "license": "MIT",
+ "keywords": [
+ "php",
+ "swoole",
+ "hyperf",
+ "database",
+ "sqlite"
+ ],
+ "homepage": "https://hyperf.io",
+ "support": {
+ "docs": "https://hyperf.wiki",
+ "issues": "https://github.com/hyperf/hyperf/issues",
+ "pull-request": "https://github.com/hyperf/hyperf/pulls",
+ "source": "https://github.com/hyperf/hyperf"
+ },
+ "require": {
+ "php": ">=8.1",
+ "hyperf/collection": "~3.1.0",
+ "hyperf/database": "~3.1.0",
+ "hyperf/support": "~3.1.0",
+ "hyperf/stringable": "~3.1.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Hyperf\\Database\\SQLite\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "HyperfTest\\Database\\SQLite\\": "tests/"
+ }
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ },
+ "hyperf": {
+ "config": "Hyperf\\Database\\SQLite\\ConfigProvider"
+ }
+ }
+}
diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php
new file mode 100644
index 0000000..8adb77b
--- /dev/null
+++ b/src/ConfigProvider.php
@@ -0,0 +1,30 @@
+ [
+ 'db.connector.sqlite' => SQLiteConnector::class,
+ ],
+ 'listeners' => [
+ RegisterConnectionListener::class,
+ ],
+ ];
+ }
+}
diff --git a/src/Connectors/SQLiteConnector.php b/src/Connectors/SQLiteConnector.php
new file mode 100644
index 0000000..ee41335
--- /dev/null
+++ b/src/Connectors/SQLiteConnector.php
@@ -0,0 +1,50 @@
+getOptions($config);
+
+ // SQLite supports "in-memory" databases that only last as long as the owning
+ // connection does. These are useful for tests or for short lifetime store
+ // querying. In-memory databases may only have a single open connection.
+ if ($config['database'] === ':memory:') {
+ return $this->createConnection('sqlite::memory:', $config, $options);
+ }
+
+ $path = realpath($config['database']);
+
+ // Here we'll verify that the SQLite database exists before going any further
+ // as the developer probably wants to know if the database exists and this
+ // SQLite driver will not throw any exception if it does not by default.
+ if ($path === false) {
+ throw new InvalidArgumentException("Database ({$config['database']}) does not exist.");
+ }
+
+ return $this->createConnection("sqlite:{$path}", $config, $options);
+ }
+}
diff --git a/src/Listener/RegisterConnectionListener.php b/src/Listener/RegisterConnectionListener.php
new file mode 100644
index 0000000..9cbeba5
--- /dev/null
+++ b/src/Listener/RegisterConnectionListener.php
@@ -0,0 +1,65 @@
+createPersistentPdoResolver($connection, $config);
+ }
+
+ return new SQLiteConnection($connection, $database, $prefix, $config);
+ });
+ }
+
+ protected function createPersistentPdoResolver($connection, $config)
+ {
+ return function () use ($config, $connection) {
+ /** @var \Hyperf\Contract\ContainerInterface $container */
+ $container = ApplicationContext::getContainer();
+ $key = "sqlite.presistent.pdo.{$config['name']}";
+
+ if (! $container->has($key)) {
+ $container->set($key, call_user_func($connection));
+ }
+
+ return $container->get($key);
+ };
+ }
+}
diff --git a/src/Query/Grammars/SQLiteGrammar.php b/src/Query/Grammars/SQLiteGrammar.php
new file mode 100644
index 0000000..3b643f6
--- /dev/null
+++ b/src/Query/Grammars/SQLiteGrammar.php
@@ -0,0 +1,301 @@
+', '<=', '>=', '<>', '!=',
+ 'like', 'not like', 'ilike',
+ '&', '|', '<<', '>>',
+ ];
+
+ /**
+ * Compile an update statement into SQL.
+ *
+ * @param array $values
+ */
+ public function compileUpdate(Builder $query, $values): string
+ {
+ if ($query->joins || $query->limit) {
+ return $this->compileUpdateWithJoinsOrLimit($query, $values);
+ }
+
+ return parent::compileUpdate($query, $values);
+ }
+
+ /**
+ * Compile an insert ignore statement into SQL.
+ */
+ public function compileInsertOrIgnore(Builder $query, array $values): string
+ {
+ return Str::replaceFirst('insert', 'insert or ignore', $this->compileInsert($query, $values));
+ }
+
+ /**
+ * Compile an "upsert" statement into SQL.
+ */
+ public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update): string
+ {
+ $sql = $this->compileInsert($query, $values);
+
+ $sql .= ' on conflict (' . $this->columnize($uniqueBy) . ') do update set ';
+
+ $columns = collect($update)->map(function ($value, $key) {
+ return is_numeric($key)
+ ? $this->wrap($value) . ' = ' . $this->wrapValue('excluded') . '.' . $this->wrap($value)
+ : $this->wrap($key) . ' = ' . $this->parameter($value);
+ })->implode(', ');
+
+ return $sql . $columns;
+ }
+
+ /**
+ * Prepare the bindings for an update statement.
+ */
+ public function prepareBindingsForUpdate(array $bindings, array $values): array
+ {
+ $groups = $this->groupJsonColumnsForUpdate($values);
+
+ $values = collect($values)->reject(function ($value, $key) {
+ return $this->isJsonSelector($key);
+ })->merge($groups)->map(function ($value) {
+ return is_array($value) ? json_encode($value) : $value;
+ })->all();
+
+ $cleanBindings = Arr::except($bindings, 'select');
+
+ return array_values(
+ array_merge($values, Arr::flatten($cleanBindings))
+ );
+ }
+
+ /**
+ * Compile a delete statement into SQL.
+ */
+ public function compileDelete(Builder $query): string
+ {
+ if ($query->joins || $query->limit) {
+ return $this->compileDeleteWithJoinsOrLimit($query);
+ }
+
+ return parent::compileDelete($query);
+ }
+
+ /**
+ * Compile a truncate table statement into SQL.
+ */
+ public function compileTruncate(Builder $query): array
+ {
+ return [
+ 'delete from sqlite_sequence where name = ?' => [$query->from],
+ 'delete from ' . $this->wrapTable($query->from) => [],
+ ];
+ }
+
+ /**
+ * Compile the lock into SQL.
+ *
+ * @param bool|string $value
+ */
+ protected function compileLock(Builder $query, $value): string
+ {
+ return '';
+ }
+
+ /**
+ * Wrap a union subquery in parentheses.
+ *
+ * @param string $sql
+ */
+ protected function wrapUnion($sql): string
+ {
+ return 'select * from (' . $sql . ')';
+ }
+
+ /**
+ * Compile a "where date" clause.
+ *
+ * @param array $where
+ */
+ protected function whereDate(Builder $query, $where): string
+ {
+ return $this->dateBasedWhere('%Y-%m-%d', $query, $where);
+ }
+
+ /**
+ * Compile a "where day" clause.
+ *
+ * @param array $where
+ */
+ protected function whereDay(Builder $query, $where): string
+ {
+ return $this->dateBasedWhere('%d', $query, $where);
+ }
+
+ /**
+ * Compile a "where month" clause.
+ *
+ * @param array $where
+ */
+ protected function whereMonth(Builder $query, $where): string
+ {
+ return $this->dateBasedWhere('%m', $query, $where);
+ }
+
+ /**
+ * Compile a "where year" clause.
+ *
+ * @param array $where
+ */
+ protected function whereYear(Builder $query, $where): string
+ {
+ return $this->dateBasedWhere('%Y', $query, $where);
+ }
+
+ /**
+ * Compile a "where time" clause.
+ *
+ * @param array $where
+ */
+ protected function whereTime(Builder $query, $where): string
+ {
+ return $this->dateBasedWhere('%H:%M:%S', $query, $where);
+ }
+
+ /**
+ * Compile a date based where clause.
+ *
+ * @param string $type
+ * @param array $where
+ */
+ protected function dateBasedWhere($type, Builder $query, $where): string
+ {
+ $value = $this->parameter($where['value']);
+
+ return "strftime('{$type}', {$this->wrap($where['column'])}) {$where['operator']} cast({$value} as text)";
+ }
+
+ /**
+ * Compile a "JSON length" statement into SQL.
+ *
+ * @param string $column
+ * @param string $operator
+ * @param string $value
+ */
+ protected function compileJsonLength($column, $operator, $value): string
+ {
+ [$field, $path] = $this->wrapJsonFieldAndPath($column);
+
+ return 'json_array_length(' . $field . $path . ') ' . $operator . ' ' . $value;
+ }
+
+ /**
+ * Compile the columns for an update statement.
+ */
+ protected function compileUpdateColumns(Builder $query, array $values): string
+ {
+ $jsonGroups = $this->groupJsonColumnsForUpdate($values);
+
+ return collect($values)->reject(function ($value, $key) {
+ return $this->isJsonSelector($key);
+ })->merge($jsonGroups)->map(function ($value, $key) use ($jsonGroups) {
+ $column = last(explode('.', $key));
+
+ $value = isset($jsonGroups[$key]) ? $this->compileJsonPatch($column, $value) : $this->parameter($value);
+
+ return $this->wrap($column) . ' = ' . $value;
+ })->implode(', ');
+ }
+
+ /**
+ * Group the nested JSON columns.
+ */
+ protected function groupJsonColumnsForUpdate(array $values): array
+ {
+ $groups = [];
+
+ foreach ($values as $key => $value) {
+ if ($this->isJsonSelector($key)) {
+ Arr::set($groups, str_replace('->', '.', Str::after($key, '.')), $value);
+ }
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Compile a "JSON" patch statement into SQL.
+ *
+ * @param string $column
+ * @param mixed $value
+ */
+ protected function compileJsonPatch($column, $value): string
+ {
+ return "json_patch(ifnull({$this->wrap($column)}, json('{}')), json({$this->parameter($value)}))";
+ }
+
+ /**
+ * Compile an update statement with joins or limit into SQL.
+ */
+ protected function compileUpdateWithJoinsOrLimit(Builder $query, array $values): string
+ {
+ $table = $this->wrapTable($query->from);
+
+ $columns = $this->compileUpdateColumns($query, $values);
+
+ $alias = last(preg_split('/\s+as\s+/i', $query->from));
+
+ $selectSql = $this->compileSelect($query->select($alias . '.rowid'));
+
+ return "update {$table} set {$columns} where {$this->wrap('rowid')} in ({$selectSql})";
+ }
+
+ /**
+ * Compile a delete statement with joins or limit into SQL.
+ */
+ protected function compileDeleteWithJoinsOrLimit(Builder $query): string
+ {
+ $table = $this->wrapTable($query->from);
+
+ $alias = last(preg_split('/\s+as\s+/i', $query->from));
+
+ $selectSql = $this->compileSelect($query->select($alias . '.rowid'));
+
+ return "delete from {$table} where {$this->wrap('rowid')} in ({$selectSql})";
+ }
+
+ /**
+ * Wrap the given JSON selector.
+ *
+ * @param string $value
+ */
+ protected function wrapJsonSelector($value): string
+ {
+ [$field, $path] = $this->wrapJsonFieldAndPath($value);
+
+ return 'json_extract(' . $field . $path . ')';
+ }
+}
diff --git a/src/Query/Processors/SQLiteProcessor.php b/src/Query/Processors/SQLiteProcessor.php
new file mode 100644
index 0000000..0e72d03
--- /dev/null
+++ b/src/Query/Processors/SQLiteProcessor.php
@@ -0,0 +1,97 @@
+name;
+ }, $results);
+ }
+
+ /**
+ * Process the results of a columns query.
+ */
+ public function processColumns(array $results): array
+ {
+ return array_map(function ($result) {
+ return new Column(
+ 'main',
+ $result['table_name'],
+ $result['column_name'],
+ $result['cid'] + 1,
+ $result['default'],
+ (bool) $result['nullable'],
+ strtok(strtolower($result['type']), '(') ?: '',
+ ''
+ );
+ }, $results);
+ }
+
+ /**
+ * Process the results of an indexes query.
+ */
+ public function processIndexes(array $results): array
+ {
+ $primaryCount = 0;
+
+ $indexes = array_map(function ($result) use (&$primaryCount) {
+ $result = (object) $result;
+
+ if ($isPrimary = (bool) $result->primary) {
+ ++$primaryCount;
+ }
+
+ return [
+ 'name' => strtolower($result->name),
+ 'columns' => explode(',', $result->columns),
+ 'type' => null,
+ 'unique' => (bool) $result->unique,
+ 'primary' => $isPrimary,
+ ];
+ }, $results);
+
+ if ($primaryCount > 1) {
+ $indexes = array_filter($indexes, fn ($index) => $index['name'] !== 'primary');
+ }
+
+ return $indexes;
+ }
+
+ /**
+ * Process the results of a foreign keys query.
+ */
+ public function processForeignKeys(array $results): array
+ {
+ return array_map(function ($result) {
+ $result = (object) $result;
+
+ return [
+ 'name' => null,
+ 'columns' => explode(',', $result->columns),
+ 'foreign_schema' => null,
+ 'foreign_table' => $result->foreign_table,
+ 'foreign_columns' => explode(',', $result->foreign_columns),
+ 'on_update' => strtolower($result->on_update),
+ 'on_delete' => strtolower($result->on_delete),
+ ];
+ }, $results);
+ }
+}
diff --git a/src/SQLiteConnection.php b/src/SQLiteConnection.php
new file mode 100644
index 0000000..261e9f3
--- /dev/null
+++ b/src/SQLiteConnection.php
@@ -0,0 +1,116 @@
+getForeignKeyConstraintsConfigurationValue();
+
+ if ($enableForeignKeyConstraints === null) {
+ return;
+ }
+
+ $enableForeignKeyConstraints
+ ? $this->getSchemaBuilder()->enableForeignKeyConstraints()
+ : $this->getSchemaBuilder()->disableForeignKeyConstraints();
+ }
+
+ /**
+ * Get a schema builder instance for the connection.
+ *
+ * @return \Hyperf\Database\SQLite\Schema\SQLiteBuilder
+ */
+ public function getSchemaBuilder(): SchemaBuilder
+ {
+ if (is_null($this->schemaGrammar)) {
+ $this->useDefaultSchemaGrammar();
+ }
+
+ return new SQLiteBuilder($this);
+ }
+
+ /**
+ * Get the default query grammar instance.
+ *
+ * @return \Hyperf\Database\SQLite\Query\Grammars\SQLiteGrammar
+ */
+ protected function getDefaultQueryGrammar(): HyperfQueryGrammar
+ {
+ /* @phpstan-ignore-next-line */
+ return $this->withTablePrefix(new QueryGrammar());
+ }
+
+ /**
+ * Get the default schema grammar instance.
+ *
+ * @return \Hyperf\Database\SQLite\Schema\Grammars\SQLiteGrammar
+ */
+ protected function getDefaultSchemaGrammar(): HyperfSchemaGrammar
+ {
+ /* @phpstan-ignore-next-line */
+ return $this->withTablePrefix(new SchemaGrammar());
+ }
+
+ /**
+ * Get the default post processor instance.
+ *
+ * @return \Hyperf\Database\SQLite\Query\Processors\SQLiteProcessor
+ */
+ protected function getDefaultPostProcessor(): Processor
+ {
+ return new SQLiteProcessor();
+ }
+
+ /**
+ * Get the Doctrine DBAL driver.
+ *
+ * @return \Doctrine\DBAL\Driver\PDO\SQLite\Driver
+ */
+ protected function getDoctrineDriver()
+ {
+ return new DoctrineDriver();
+ }
+
+ /**
+ * Get the database connection foreign key constraints configuration option.
+ *
+ * @return null|bool
+ */
+ protected function getForeignKeyConstraintsConfigurationValue()
+ {
+ return $this->getConfig('foreign_key_constraints');
+ }
+}
diff --git a/src/Schema/Grammars/SQLiteGrammar.php b/src/Schema/Grammars/SQLiteGrammar.php
new file mode 100644
index 0000000..88a7e9e
--- /dev/null
+++ b/src/Schema/Grammars/SQLiteGrammar.php
@@ -0,0 +1,1012 @@
+wrap(str_replace('.', '__', $table)) . ')';
+ }
+
+ /**
+ * Compile the query to determine the columns.
+ */
+ public function compileColumns(string $table): string
+ {
+ return sprintf(
+ 'select %s as "table_name", name as "column_name", type, not "notnull" as "nullable", dflt_value as "default", pk as "primary", cid '
+ . 'from pragma_table_info(%s) order by cid asc',
+ $tableName = $this->wrap(str_replace('.', '__', $table)),
+ $tableName
+ );
+ }
+
+ /**
+ * Compile the query to determine the indexes.
+ */
+ public function compileIndexes(string $table): string
+ {
+ return sprintf(
+ 'select "primary" as name, group_concat(col) as columns, 1 as "unique", 1 as "primary" '
+ . 'from (select name as col from pragma_table_info(%s) where pk > 0 order by pk, cid) group by name '
+ . 'union select name, group_concat(col) as columns, "unique", origin = "pk" as "primary" '
+ . 'from (select il.*, ii.name as col from pragma_index_list(%s) il, pragma_index_info(il.name) ii order by il.seq, ii.seqno) '
+ . 'group by name, "unique", "primary"',
+ $table = $this->wrap(str_replace('.', '__', $table)),
+ $table
+ );
+ }
+
+ /**
+ * Compile the query to determine the foreign keys.
+ */
+ public function compileForeignKeys(string $table): string
+ {
+ return sprintf(
+ 'select group_concat("from") as columns, "table" as foreign_table, '
+ . 'group_concat("to") as foreign_columns, on_update, on_delete '
+ . 'from (select * from pragma_foreign_key_list(%s) order by id desc, seq) '
+ . 'group by id, "table", on_update, on_delete',
+ $this->wrap(str_replace('.', '__', $table))
+ );
+ }
+
+ /**
+ * Compile a create table command.
+ */
+ public function compileCreate(Blueprint $blueprint, Fluent $command): string
+ {
+ return sprintf(
+ '%s table %s (%s%s%s)',
+ $blueprint->temporary ? 'create temporary' : 'create',
+ $this->wrapTable($blueprint),
+ implode(', ', $this->getColumns($blueprint)),
+ (string) $this->addForeignKeys($blueprint),
+ (string) $this->addPrimaryKeys($blueprint)
+ );
+ }
+
+ /**
+ * Compile alter table commands for adding columns.
+ */
+ public function compileAdd(Blueprint $blueprint, Fluent $command): array
+ {
+ $columns = $this->prefixArray('add column', $this->getColumns($blueprint));
+
+ return collect($columns)->reject(function ($column) {
+ return preg_match('/as \(.*\) stored/', $column) > 0;
+ })->map(function ($column) use ($blueprint) {
+ return 'alter table ' . $this->wrapTable($blueprint) . ' ' . $column;
+ })->all();
+ }
+
+ /**
+ * Compile a unique key command.
+ */
+ public function compileUnique(Blueprint $blueprint, Fluent $command): string
+ {
+ return sprintf(
+ 'create unique index %s on %s (%s)',
+ $this->wrap($command->index),
+ $this->wrapTable($blueprint),
+ $this->columnize($command->columns)
+ );
+ }
+
+ /**
+ * Compile a plain index key command.
+ */
+ public function compileIndex(Blueprint $blueprint, Fluent $command): string
+ {
+ return sprintf(
+ 'create index %s on %s (%s)',
+ $this->wrap($command->index),
+ $this->wrapTable($blueprint),
+ $this->columnize($command->columns)
+ );
+ }
+
+ /**
+ * Compile a spatial index key command.
+ *
+ * @throws RuntimeException
+ */
+ public function compileSpatialIndex(Blueprint $blueprint, Fluent $command): void
+ {
+ throw new RuntimeException('The database driver in use does not support spatial indexes.');
+ }
+
+ /**
+ * Compile a foreign key command.
+ */
+ public function compileForeign(Blueprint $blueprint, Fluent $command): string
+ {
+ // Handled on table creation...
+ return '';
+ }
+
+ /**
+ * Compile a drop table command.
+ */
+ public function compileDrop(Blueprint $blueprint, Fluent $command): string
+ {
+ return 'drop table ' . $this->wrapTable($blueprint);
+ }
+
+ /**
+ * Compile a drop table (if exists) command.
+ */
+ public function compileDropIfExists(Blueprint $blueprint, Fluent $command): string
+ {
+ return 'drop table if exists ' . $this->wrapTable($blueprint);
+ }
+
+ /**
+ * Compile the SQL needed to drop all tables.
+ */
+ public function compileDropAllTables(): string
+ {
+ return "delete from sqlite_master where type in ('table', 'index', 'trigger')";
+ }
+
+ /**
+ * Compile the SQL needed to drop all views.
+ */
+ public function compileDropAllViews(): string
+ {
+ return "delete from sqlite_master where type in ('view')";
+ }
+
+ /**
+ * Compile the SQL needed to rebuild the database.
+ */
+ public function compileRebuild(): string
+ {
+ return 'vacuum';
+ }
+
+ /**
+ * Compile a drop column command.
+ */
+ public function compileDropColumn(Blueprint $blueprint, Fluent $command): array
+ {
+ $table = $this->wrapTable($blueprint);
+
+ $columns = $this->prefixArray('drop column', $this->wrapArray($command->columns));
+
+ return collect($columns)
+ ->map(
+ fn ($column) => 'alter table ' . $table . ' ' . $column
+ )->all();
+ }
+
+ /**
+ * Compile a drop unique key command.
+ */
+ public function compileDropUnique(Blueprint $blueprint, Fluent $command): string
+ {
+ $index = $this->wrap($command->index);
+
+ return "drop index {$index}";
+ }
+
+ /**
+ * Compile a drop index command.
+ */
+ public function compileDropIndex(Blueprint $blueprint, Fluent $command): string
+ {
+ $index = $this->wrap($command->index);
+
+ return "drop index {$index}";
+ }
+
+ /**
+ * Compile a drop spatial index command.
+ *
+ * @throws RuntimeException
+ */
+ public function compileDropSpatialIndex(Blueprint $blueprint, Fluent $command): void
+ {
+ throw new RuntimeException('The database driver in use does not support spatial indexes.');
+ }
+
+ /**
+ * Compile a rename table command.
+ */
+ public function compileRename(Blueprint $blueprint, Fluent $command): string
+ {
+ $from = $this->wrapTable($blueprint);
+
+ return "alter table {$from} rename to " . $this->wrapTable($command->to);
+ }
+
+ /**
+ * Compile a rename index command.
+ *
+ * @throws RuntimeException
+ */
+ public function compileRenameIndex(Blueprint $blueprint, Fluent $command, Connection $connection): array
+ {
+ $indexes = $this->getIndexColumns($connection, $blueprint->getTable());
+ if (! $index = Arr::get($indexes, $command->from)) {
+ throw new RuntimeException("Index [{$command->from}] does not exist.");
+ }
+
+ $compileMethod = $index['unique'] ? 'compileUnique' : 'compileIndex';
+
+ return [
+ $this->compileDropIndex($blueprint, new Fluent(['index' => $command->from])),
+ $this->{$compileMethod}(
+ $blueprint,
+ new Fluent(['index' => $command->to, 'columns' => $index['columns']])
+ )];
+ }
+
+ /**
+ * Compile the command to enable foreign key constraints.
+ */
+ public function compileEnableForeignKeyConstraints(): string
+ {
+ return 'PRAGMA foreign_keys = ON;';
+ }
+
+ /**
+ * Compile the command to disable foreign key constraints.
+ */
+ public function compileDisableForeignKeyConstraints(): string
+ {
+ return 'PRAGMA foreign_keys = OFF;';
+ }
+
+ /**
+ * Compile the SQL needed to enable a writable schema.
+ */
+ public function compileEnableWriteableSchema(): string
+ {
+ return 'PRAGMA writable_schema = 1;';
+ }
+
+ /**
+ * Compile the SQL needed to disable a writable schema.
+ */
+ public function compileDisableWriteableSchema(): string
+ {
+ return 'PRAGMA writable_schema = 0;';
+ }
+
+ /**
+ * Create the column definition for a spatial Geometry type.
+ */
+ public function typeGeometry(Fluent $column): string
+ {
+ return 'geometry';
+ }
+
+ /**
+ * Create the column definition for a spatial Point type.
+ */
+ public function typePoint(Fluent $column): string
+ {
+ return 'point';
+ }
+
+ /**
+ * Create the column definition for a spatial LineString type.
+ */
+ public function typeLineString(Fluent $column): string
+ {
+ return 'linestring';
+ }
+
+ /**
+ * Create the column definition for a spatial Polygon type.
+ */
+ public function typePolygon(Fluent $column): string
+ {
+ return 'polygon';
+ }
+
+ /**
+ * Create the column definition for a spatial GeometryCollection type.
+ */
+ public function typeGeometryCollection(Fluent $column): string
+ {
+ return 'geometrycollection';
+ }
+
+ /**
+ * Create the column definition for a spatial MultiPoint type.
+ */
+ public function typeMultiPoint(Fluent $column): string
+ {
+ return 'multipoint';
+ }
+
+ /**
+ * Create the column definition for a spatial MultiLineString type.
+ */
+ public function typeMultiLineString(Fluent $column): string
+ {
+ return 'multilinestring';
+ }
+
+ /**
+ * Create the column definition for a spatial MultiPolygon type.
+ */
+ public function typeMultiPolygon(Fluent $column): string
+ {
+ return 'multipolygon';
+ }
+
+ protected function getIndexColumns(Connection $connection, string $tableName = null): array
+ {
+ $sql = <<<'SQL'
+ SELECT t.name AS table_name,
+ i.*
+ FROM sqlite_master t
+ JOIN pragma_index_list(t.name) i
+SQL;
+
+ $conditions = [
+ "t.type = 'table'",
+ "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')",
+ ];
+
+ if ($tableName !== null) {
+ $tableName = str_replace('.', '__', $tableName);
+ $conditions[] = "t.name = '{$tableName}'";
+ }
+
+ $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, i.seq';
+
+ return $this->getTableIndexesList(
+ $connection,
+ $connection->select($sql),
+ $tableName
+ );
+ }
+
+ /**
+ * @see http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
+ */
+ protected function getTableIndexesList(Connection $connection, array $tableIndexes, string $tableName = null): array
+ {
+ $indexBuffer = [];
+
+ // fetch primary
+ $indexArray = $connection->select("SELECT * FROM PRAGMA_TABLE_INFO ('{$tableName}')");
+
+ usort(
+ $indexArray,
+ static function ($a, $b): int {
+ if ($a->pk === $b->pk) {
+ return $a->cid - $b->cid;
+ }
+
+ return $a->pk - $b->pk;
+ },
+ );
+
+ foreach ($indexArray as $indexColumnRow) {
+ if ($indexColumnRow->pk === 0 || $indexColumnRow->pk === '0') {
+ continue;
+ }
+
+ $indexBuffer[] = [
+ 'key_name' => 'primary',
+ 'primary' => true,
+ 'non_unique' => false,
+ 'column_name' => $indexColumnRow->name,
+ 'where' => null,
+ 'flags' => null,
+ 'length' => null,
+ ];
+ }
+
+ // fetch regular indexes
+ foreach ($tableIndexes as $tableIndex) {
+ // Ignore indexes with reserved names, e.g. autoindexes
+ if (str_starts_with($tableIndex->name, 'sqlite_')) {
+ continue;
+ }
+
+ $keyName = $tableIndex->name;
+ $idx = [];
+ $idx['key_name'] = $keyName;
+ $idx['primary'] = false;
+ $idx['non_unique'] = ! $tableIndex->unique;
+
+ $indexArray = $connection->select("SELECT * FROM PRAGMA_INDEX_INFO ('{$keyName}')");
+
+ foreach ($indexArray as $indexColumnRow) {
+ $idx['column_name'] = $indexColumnRow->name;
+ $indexBuffer[] = $idx;
+ }
+ }
+
+ $result = [];
+ foreach ($indexBuffer as $tableIndex) {
+ $indexName = $keyName = $tableIndex['key_name'];
+ if ($tableIndex['primary']) {
+ $keyName = 'primary';
+ }
+
+ $keyName = strtolower($keyName);
+
+ if (! isset($result[$keyName])) {
+ $options = [
+ 'lengths' => [],
+ ];
+
+ if (isset($tableIndex['where'])) {
+ $options['where'] = $tableIndex['where'];
+ }
+
+ $result[$keyName] = [
+ 'name' => $indexName,
+ 'columns' => [],
+ 'unique' => ! $tableIndex['non_unique'],
+ 'primary' => $tableIndex['primary'],
+ 'flags' => $tableIndex['flags'] ?? [],
+ 'options' => $options,
+ ];
+ }
+
+ $result[$keyName]['columns'][] = $tableIndex['column_name'];
+ $result[$keyName]['options']['lengths'][] = $tableIndex['length'] ?? null;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get the foreign key syntax for a table creation statement.
+ */
+ protected function addForeignKeys(Blueprint $blueprint): ?string
+ {
+ $foreigns = $this->getCommandsByName($blueprint, 'foreign');
+
+ return collect($foreigns)->reduce(function ($sql, $foreign) {
+ // Once we have all the foreign key commands for the table creation statement
+ // we'll loop through each of them and add them to the create table SQL we
+ // are building, since SQLite needs foreign keys on the tables creation.
+ $sql .= $this->getForeignKey($foreign);
+
+ if (! is_null($foreign->onDelete)) {
+ $sql .= " on delete {$foreign->onDelete}";
+ }
+
+ // If this foreign key specifies the action to be taken on update we will add
+ // that to the statement here. We'll append it to this SQL and then return
+ // the SQL so we can keep adding any other foreign constraints onto this.
+ if (! is_null($foreign->onUpdate)) {
+ $sql .= " on update {$foreign->onUpdate}";
+ }
+
+ return $sql;
+ }, '');
+ }
+
+ /**
+ * Get the SQL for the foreign key.
+ *
+ * @param \Hyperf\Support\Fluent $foreign
+ */
+ protected function getForeignKey($foreign): string
+ {
+ // We need to columnize the columns that the foreign key is being defined for
+ // so that it is a properly formatted list. Once we have done this, we can
+ // return the foreign key SQL declaration to the calling method for use.
+ return sprintf(
+ ', foreign key(%s) references %s(%s)',
+ $this->columnize($foreign->columns),
+ $this->wrapTable($foreign->on),
+ $this->columnize((array) $foreign->references)
+ );
+ }
+
+ /**
+ * Get the primary key syntax for a table creation statement.
+ */
+ protected function addPrimaryKeys(Blueprint $blueprint): ?string
+ {
+ if (! is_null($primary = $this->getCommandByName($blueprint, 'primary'))) {
+ return ", primary key ({$this->columnize($primary->columns)})";
+ }
+
+ return null;
+ }
+
+ /**
+ * Create the column definition for a char type.
+ */
+ protected function typeChar(Fluent $column): string
+ {
+ return 'varchar';
+ }
+
+ /**
+ * Create the column definition for a string type.
+ */
+ protected function typeString(Fluent $column): string
+ {
+ return 'varchar';
+ }
+
+ /**
+ * Create the column definition for a tiny text type.
+ */
+ protected function typeTinyText(Fluent $column): string
+ {
+ return 'text';
+ }
+
+ /**
+ * Create the column definition for a text type.
+ */
+ protected function typeText(Fluent $column): string
+ {
+ return 'text';
+ }
+
+ /**
+ * Create the column definition for a medium text type.
+ */
+ protected function typeMediumText(Fluent $column): string
+ {
+ return 'text';
+ }
+
+ /**
+ * Create the column definition for a long text type.
+ */
+ protected function typeLongText(Fluent $column): string
+ {
+ return 'text';
+ }
+
+ /**
+ * Create the column definition for an integer type.
+ */
+ protected function typeInteger(Fluent $column): string
+ {
+ return 'integer';
+ }
+
+ /**
+ * Create the column definition for a big integer type.
+ */
+ protected function typeBigInteger(Fluent $column): string
+ {
+ return 'integer';
+ }
+
+ /**
+ * Create the column definition for a medium integer type.
+ */
+ protected function typeMediumInteger(Fluent $column): string
+ {
+ return 'integer';
+ }
+
+ /**
+ * Create the column definition for a tiny integer type.
+ */
+ protected function typeTinyInteger(Fluent $column): string
+ {
+ return 'integer';
+ }
+
+ /**
+ * Create the column definition for a small integer type.
+ */
+ protected function typeSmallInteger(Fluent $column): string
+ {
+ return 'integer';
+ }
+
+ /**
+ * Create the column definition for a float type.
+ */
+ protected function typeFloat(Fluent $column): string
+ {
+ return 'float';
+ }
+
+ /**
+ * Create the column definition for a double type.
+ */
+ protected function typeDouble(Fluent $column): string
+ {
+ return 'float';
+ }
+
+ /**
+ * Create the column definition for a decimal type.
+ */
+ protected function typeDecimal(Fluent $column): string
+ {
+ return 'numeric';
+ }
+
+ /**
+ * Create the column definition for a boolean type.
+ */
+ protected function typeBoolean(Fluent $column): string
+ {
+ return 'tinyint(1)';
+ }
+
+ /**
+ * Create the column definition for an enumeration type.
+ */
+ protected function typeEnum(Fluent $column): string
+ {
+ return sprintf(
+ 'varchar check ("%s" in (%s))',
+ $column->name,
+ $this->quoteString($column->allowed)
+ );
+ }
+
+ /**
+ * Create the column definition for a json type.
+ */
+ protected function typeJson(Fluent $column): string
+ {
+ return 'text';
+ }
+
+ /**
+ * Create the column definition for a jsonb type.
+ */
+ protected function typeJsonb(Fluent $column): string
+ {
+ return 'text';
+ }
+
+ /**
+ * Create the column definition for a date type.
+ */
+ protected function typeDate(Fluent $column): string
+ {
+ return 'date';
+ }
+
+ /**
+ * Create the column definition for a date-time type.
+ */
+ protected function typeDateTime(Fluent $column): string
+ {
+ return $this->typeTimestamp($column);
+ }
+
+ /**
+ * Create the column definition for a date-time (with time zone) type.
+ *
+ * Note: "SQLite does not have a storage class set aside for storing dates and/or times."
+ *
+ * @see https://www.sqlite.org/datatype3.html
+ */
+ protected function typeDateTimeTz(Fluent $column): string
+ {
+ return $this->typeDateTime($column);
+ }
+
+ /**
+ * Create the column definition for a time type.
+ */
+ protected function typeTime(Fluent $column): string
+ {
+ return 'time';
+ }
+
+ /**
+ * Create the column definition for a time (with time zone) type.
+ */
+ protected function typeTimeTz(Fluent $column): string
+ {
+ return $this->typeTime($column);
+ }
+
+ /**
+ * Create the column definition for a timestamp type.
+ */
+ protected function typeTimestamp(Fluent $column): string
+ {
+ return $column->useCurrent ? 'datetime default CURRENT_TIMESTAMP' : 'datetime';
+ }
+
+ /**
+ * Create the column definition for a timestamp (with time zone) type.
+ */
+ protected function typeTimestampTz(Fluent $column): string
+ {
+ return $this->typeTimestamp($column);
+ }
+
+ /**
+ * Create the column definition for a year type.
+ */
+ protected function typeYear(Fluent $column): string
+ {
+ return $this->typeInteger($column);
+ }
+
+ /**
+ * Create the column definition for a binary type.
+ */
+ protected function typeBinary(Fluent $column): string
+ {
+ return 'blob';
+ }
+
+ /**
+ * Create the column definition for a uuid type.
+ */
+ protected function typeUuid(Fluent $column): string
+ {
+ return 'varchar';
+ }
+
+ /**
+ * Create the column definition for an IP address type.
+ */
+ protected function typeIpAddress(Fluent $column): string
+ {
+ return 'varchar';
+ }
+
+ /**
+ * Create the column definition for a MAC address type.
+ */
+ protected function typeMacAddress(Fluent $column): string
+ {
+ return 'varchar';
+ }
+
+ /**
+ * Create the column definition for a generated, computed column type.
+ *
+ * @throws RuntimeException
+ */
+ protected function typeComputed(Fluent $column): void
+ {
+ throw new RuntimeException('This database driver requires a type, see the virtualAs / storedAs modifiers.');
+ }
+
+ /**
+ * Get the SQL for a generated virtual column modifier.
+ */
+ protected function modifyVirtualAs(Blueprint $blueprint, Fluent $column): ?string
+ {
+ if (! is_null($virtualAs = $column->virtualAsJson)) {
+ if ($this->isJsonSelector($virtualAs)) {
+ $virtualAs = $this->wrapJsonSelector($virtualAs);
+ }
+
+ return " as ({$virtualAs})";
+ }
+
+ if (! is_null($column->virtualAs)) {
+ return " as ({$column->virtualAs})";
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the SQL for a generated stored column modifier.
+ */
+ protected function modifyStoredAs(Blueprint $blueprint, Fluent $column): ?string
+ {
+ if (! is_null($storedAs = $column->storedAsJson)) {
+ if ($this->isJsonSelector($storedAs)) {
+ $storedAs = $this->wrapJsonSelector($storedAs);
+ }
+
+ return " as ({$storedAs}) stored";
+ }
+
+ if (! is_null($column->storedAs)) {
+ return " as ({$column->storedAs}) stored";
+ }
+
+ return null;
+ }
+
+ /**
+ * Determine if the given string is a JSON selector.
+ */
+ protected function isJsonSelector(string $value): bool
+ {
+ return str_contains($value, '->');
+ }
+
+ /**
+ * Wrap the given JSON selector.
+ */
+ protected function wrapJsonSelector(string $value): string
+ {
+ [$field, $path] = $this->wrapJsonFieldAndPath($value);
+
+ return 'json_extract(' . $field . $path . ')';
+ }
+
+ /**
+ * Split the given JSON selector into the field and the optional path and wrap them separately.
+ */
+ protected function wrapJsonFieldAndPath(string $column): array
+ {
+ $parts = explode('->', $column, 2);
+
+ $field = $this->wrap($parts[0]);
+
+ $path = count($parts) > 1 ? ', ' . $this->wrapJsonPath($parts[1], '->') : '';
+
+ return [$field, $path];
+ }
+
+ /**
+ * Wrap the given JSON path.
+ */
+ protected function wrapJsonPath(string $value, string $delimiter = '->'): string
+ {
+ $value = preg_replace("/([\\\\]+)?\\'/", "''", $value);
+
+ $jsonPath = collect(explode($delimiter, $value))
+ ->map(fn ($segment) => $this->wrapJsonPathSegment($segment))
+ ->implode('.');
+
+ return "'$" . (str_starts_with($jsonPath, '[') ? '' : '.') . $jsonPath . "'";
+ }
+
+ /**
+ * Wrap the given JSON path segment.
+ */
+ protected function wrapJsonPathSegment(string $segment): string
+ {
+ if (preg_match('/(\[[^\]]+\])+$/', $segment, $parts)) {
+ $key = Str::beforeLast($segment, $parts[0]);
+
+ if (! empty($key)) {
+ return '"' . $key . '"' . $parts[0];
+ }
+
+ return $parts[0];
+ }
+
+ return '"' . $segment . '"';
+ }
+
+ /**
+ * Get the SQL for a nullable column modifier.
+ */
+ protected function modifyNullable(Blueprint $blueprint, Fluent $column): ?string
+ {
+ if (is_null($column->virtualAs) && is_null($column->storedAs)) {
+ return $column->nullable ? '' : ' not null';
+ }
+
+ if ($column->nullable === false) {
+ return ' not null';
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the SQL for a default column modifier.
+ */
+ protected function modifyDefault(Blueprint $blueprint, Fluent $column): ?string
+ {
+ if (! is_null($column->default) && is_null($column->virtualAs) && is_null($column->storedAs)) {
+ return ' default ' . $this->getDefaultValue($column->default);
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the SQL for an auto-increment column modifier.
+ */
+ protected function modifyIncrement(Blueprint $blueprint, Fluent $column): ?string
+ {
+ if (in_array($column->type, $this->serials) && $column->autoIncrement) {
+ return ' primary key autoincrement';
+ }
+
+ return null;
+ }
+}
diff --git a/src/Schema/SQLiteBuilder.php b/src/Schema/SQLiteBuilder.php
new file mode 100644
index 0000000..cce4461
--- /dev/null
+++ b/src/Schema/SQLiteBuilder.php
@@ -0,0 +1,150 @@
+getFilesystem()
+ ->put($name, '');
+ }
+
+ /**
+ * Drop a database from the schema if the database exists.
+ *
+ * @param string $name
+ */
+ public function dropDatabaseIfExists($name): bool
+ {
+ $file = $this->getFilesystem();
+
+ return $file->exists($name)
+ ? $file->delete($name)
+ : true;
+ }
+
+ /**
+ * Get the column type listing for a given table.
+ *
+ * @param string $table
+ */
+ public function getColumnTypeListing($table): array
+ {
+ $table = $this->connection->getTablePrefix() . $table;
+
+ return $this->connection->getPostProcessor()->processColumnListing(
+ $this->connection->select(
+ $this->grammar->compileColumnListing($table)
+ )
+ );
+ }
+
+ /**
+ * Get the indexes for a given table.
+ */
+ public function getIndexes(string $table): array
+ {
+ $table = $this->connection->getTablePrefix() . $table;
+
+ return $this->connection->getPostProcessor()->processIndexes(
+ $this->connection->selectFromWriteConnection($this->grammar->compileIndexes($table))
+ );
+ }
+
+ /**
+ * Get the names of the indexes for a given table.
+ */
+ public function getIndexListing(string $table): array
+ {
+ return array_column($this->getIndexes($table), 'name');
+ }
+
+ /**
+ * Determine if the given table has a given index.
+ *
+ * @param array|string $index
+ */
+ public function hasIndex(string $table, $index, string $type = null): bool
+ {
+ $type = is_null($type) ? $type : strtolower($type);
+
+ foreach ($this->getIndexes($table) as $value) {
+ $typeMatches = is_null($type)
+ || ($type === 'primary' && $value['primary'])
+ || ($type === 'unique' && $value['unique'])
+ || $type === $value['type'];
+
+ if (($value['name'] === $index || $value['columns'] === $index) && $typeMatches) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Drop all tables from the database.
+ */
+ public function dropAllTables(): void
+ {
+ if ($this->connection->getDatabaseName() !== ':memory:') {
+ $this->refreshDatabaseFile();
+ }
+
+ $this->connection->select($this->grammar->compileEnableWriteableSchema());
+
+ $this->connection->select($this->grammar->compileDropAllTables());
+
+ $this->connection->select($this->grammar->compileDisableWriteableSchema());
+
+ $this->connection->select($this->grammar->compileRebuild());
+ }
+
+ /**
+ * Drop all views from the database.
+ */
+ public function dropAllViews(): void
+ {
+ $this->connection->select($this->grammar->compileEnableWriteableSchema());
+
+ $this->connection->select($this->grammar->compileDropAllViews());
+
+ $this->connection->select($this->grammar->compileDisableWriteableSchema());
+
+ $this->connection->select($this->grammar->compileRebuild());
+ }
+
+ /**
+ * Empty the database file.
+ */
+ public function refreshDatabaseFile(): void
+ {
+ $this->getFilesystem()
+ ->put($this->connection->getDatabaseName(), '');
+ }
+
+ protected function getFilesystem(): Filesystem
+ {
+ return ApplicationContext::getContainer()
+ ->get(Filesystem::class);
+ }
+}
diff --git a/tests/DatabaseSQLiteBuilderTest.php b/tests/DatabaseSQLiteBuilderTest.php
new file mode 100644
index 0000000..7caca32
--- /dev/null
+++ b/tests/DatabaseSQLiteBuilderTest.php
@@ -0,0 +1,99 @@
+shouldReceive('put')
+ ->once()
+ ->with('my_temporary_database_a', '')
+ ->andReturn(20);
+
+ $container = m::mock(ContainerInterface::class);
+ $container->shouldReceive('get')
+ ->with(Filesystem::class)
+ ->andReturn($filesystem);
+ ApplicationContext::setContainer($container);
+
+ $connection = m::mock(Connection::class);
+ $connection->shouldReceive('getSchemaGrammar')->once();
+
+ $builder = new SQLiteBuilder($connection);
+
+ $this->assertTrue($builder->createDatabase('my_temporary_database_a'));
+
+ $filesystem->shouldReceive('put')
+ ->once()
+ ->with('my_temporary_database_b', '')
+ ->andReturn(false);
+
+ $this->assertFalse($builder->createDatabase('my_temporary_database_b'));
+ }
+
+ public function testDropDatabaseIfExists()
+ {
+ $filesystem = m::mock(Filesystem::class);
+ $filesystem->shouldReceive('exists')
+ ->once()
+ ->andReturn(true);
+
+ $filesystem->shouldReceive('delete')
+ ->once()
+ ->with('my_temporary_database_b')
+ ->andReturn(true);
+
+ $container = m::mock(ContainerInterface::class);
+ $container->shouldReceive('get')
+ ->with(Filesystem::class)
+ ->andReturn($filesystem);
+ ApplicationContext::setContainer($container);
+
+ $connection = m::mock(Connection::class);
+ $connection->shouldReceive('getSchemaGrammar')->once();
+
+ $builder = new SQLiteBuilder($connection);
+
+ $this->assertTrue($builder->dropDatabaseIfExists('my_temporary_database_b'));
+
+ $filesystem->shouldReceive('exists')
+ ->once()
+ ->andReturn(false);
+
+ $this->assertTrue($builder->dropDatabaseIfExists('my_temporary_database_c'));
+
+ $filesystem->shouldReceive('exists')
+ ->once()
+ ->andReturn(true);
+
+ $filesystem->shouldReceive('delete')
+ ->once()
+ ->with('my_temporary_database_c')
+ ->andReturn(false);
+
+ $this->assertFalse($builder->dropDatabaseIfExists('my_temporary_database_c'));
+ }
+}
diff --git a/tests/DatabaseSQLiteProcessorTest.php b/tests/DatabaseSQLiteProcessorTest.php
new file mode 100644
index 0000000..7409b5f
--- /dev/null
+++ b/tests/DatabaseSQLiteProcessorTest.php
@@ -0,0 +1,57 @@
+ 'id', 'type' => 'INTEGER', 'notnull' => '0', 'default' => '', 'pk' => '1'],
+ ['name' => 'name', 'type' => 'varchar', 'notnull' => '1', 'default' => 'foo', 'pk' => '0'],
+ ['name' => 'is_active', 'type' => 'tinyint(1)', 'notnull' => '0', 'default' => '1', 'pk' => '0'],
+ ];
+ $expected = ['id', 'name', 'is_active'];
+
+ $this->assertSame($expected, $processor->processColumnListing($listing));
+ }
+
+ public function testProcessColumns()
+ {
+ $processor = new SQLiteProcessor();
+
+ $listing = [
+ ['table_name' => 'foo', 'column_name' => 'id', 'type' => 'INTEGER', 'nullable' => '0', 'default' => '', 'primary' => '1', 'cid' => 0],
+ ['table_name' => 'foo', 'column_name' => 'name', 'type' => 'varchar', 'nullable' => '1', 'default' => 'foo', 'primary' => '0', 'cid' => 1],
+ ['table_name' => 'foo', 'column_name' => 'is_active', 'type' => 'tinyint(1)', 'nullable' => '0', 'default' => '1', 'primary' => '0', 'cid' => 2],
+ ];
+
+ $this->assertSame(3, count($columns = $processor->processColumns($listing)));
+ $this->assertInstanceOf(Column::class, $column = $columns[0]);
+ $this->assertSame('foo', $column->getTable());
+ $this->assertSame('id', $column->getName());
+ $this->assertSame(1, $column->getPosition());
+ $this->assertSame('integer', $column->getType());
+ $this->assertEmpty($column->getDefault());
+ $this->assertFalse($column->isNullable());
+ }
+}
diff --git a/tests/DatabaseSQLiteQueryGrammarTest.php b/tests/DatabaseSQLiteQueryGrammarTest.php
new file mode 100644
index 0000000..22a8677
--- /dev/null
+++ b/tests/DatabaseSQLiteQueryGrammarTest.php
@@ -0,0 +1,45 @@
+shouldReceive('escape')->with('foo', false)->andReturn("'foo'");
+ $grammar = new SQLiteGrammar();
+
+ $bindings = array_map(fn ($value) => $connection->escape($value, false), ['foo']);
+
+ $query = $grammar->substituteBindingsIntoRawSql(
+ 'select * from "users" where \'Hello\'\'World?\' IS NOT NULL AND "email" = ?',
+ $bindings,
+ );
+
+ $this->assertSame('select * from "users" where \'Hello\'\'World?\' IS NOT NULL AND "email" = \'foo\'', $query);
+ }
+}
diff --git a/tests/DatabaseSQLiteSchemaGrammarTest.php b/tests/DatabaseSQLiteSchemaGrammarTest.php
new file mode 100644
index 0000000..45275c5
--- /dev/null
+++ b/tests/DatabaseSQLiteSchemaGrammarTest.php
@@ -0,0 +1,939 @@
+create();
+ $blueprint->increments('id');
+ $blueprint->string('email');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('create table "users" ("id" integer primary key autoincrement not null, "email" varchar not null)', $statements[0]);
+
+ $blueprint = new Blueprint('users');
+ $blueprint->increments('id');
+ $blueprint->string('email');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(2, $statements);
+ $expected = [
+ 'alter table "users" add column "id" integer primary key autoincrement not null',
+ 'alter table "users" add column "email" varchar not null',
+ ];
+ $this->assertEquals($expected, $statements);
+ }
+
+ public function testCreateTemporaryTable()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->create();
+ $blueprint->temporary();
+ $blueprint->increments('id');
+ $blueprint->string('email');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('create temporary table "users" ("id" integer primary key autoincrement not null, "email" varchar not null)', $statements[0]);
+ }
+
+ public function testDropTable()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->drop();
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('drop table "users"', $statements[0]);
+ }
+
+ public function testDropTableIfExists()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->dropIfExists();
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('drop table if exists "users"', $statements[0]);
+ }
+
+ public function testDropUnique()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->dropUnique('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('drop index "foo"', $statements[0]);
+ }
+
+ public function testDropIndex()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->dropIndex('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('drop index "foo"', $statements[0]);
+ }
+
+ public function testDropColumn()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->dropColumn('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" drop column "foo"', $statements[0]);
+
+ $blueprint = new Blueprint('users');
+ $blueprint->dropColumn(['foo', 'bar']);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(2, $statements);
+ $this->assertSame('alter table "users" drop column "foo"', $statements[0]);
+ $this->assertSame('alter table "users" drop column "bar"', $statements[1]);
+ }
+
+ public function testDropSpatialIndex()
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('The database driver in use does not support spatial indexes.');
+
+ $blueprint = new Blueprint('geo');
+ $blueprint->dropSpatialIndex(['coordinates']);
+ $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ }
+
+ public function testRenameTable()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->rename('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" rename to "foo"', $statements[0]);
+ }
+
+ public function testRenameIndex()
+ {
+ $pdo = (new SQLiteConnector())
+ ->connect([
+ 'driver' => 'sqlite',
+ 'database' => ':memory:',
+ 'prefix' => 'prefix_',
+ ]);
+ $connection = new SQLiteConnection($pdo);
+ $resolver = new ConnectionResolver([
+ 'default' => $connection,
+ ]);
+
+ $schema = $resolver->connection()->getSchemaBuilder();
+
+ $schema->create('users', function (Blueprint $table) {
+ $table->string('name');
+ $table->string('email');
+ });
+
+ $schema->table('users', function (Blueprint $table) {
+ $table->index(['name', 'email'], 'index1');
+ });
+
+ $indexes = $schema->getIndexListing('users');
+
+ $this->assertContains('index1', $indexes);
+ $this->assertNotContains('index2', $indexes);
+
+ $schema->table('users', function (Blueprint $table) {
+ $table->renameIndex('index1', 'index2');
+ });
+
+ $this->assertFalse($schema->hasIndex('users', 'index1'));
+ $this->assertTrue(Collection::make($schema->getIndexes('users'))->contains(
+ fn ($index) => $index['name'] === 'index2' && $index['columns'] === ['name', 'email']
+ ));
+ }
+
+ public function testAddingPrimaryKey()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->create();
+ $blueprint->string('foo')->primary();
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('create table "users" ("foo" varchar not null, primary key ("foo"))', $statements[0]);
+ }
+
+ public function testAddingForeignKey()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->create();
+ $blueprint->string('foo')->primary();
+ $blueprint->string('order_id');
+ $blueprint->foreign('order_id')->references('id')->on('orders');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ // because string return type in Hyperf\Database\Schema\Grammars\Grammar::compileForeign is a must
+ // it has a redundancy statement
+ $this->assertCount(2, $statements);
+ $this->assertSame('create table "users" ("foo" varchar not null, "order_id" varchar not null, foreign key("order_id") references "orders"("id"), primary key ("foo"))', $statements[0]);
+ }
+
+ public function testAddingUniqueKey()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->unique('foo', 'bar');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('create unique index "bar" on "users" ("foo")', $statements[0]);
+ }
+
+ public function testAddingIndex()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->index(['foo', 'bar'], 'baz');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('create index "baz" on "users" ("foo", "bar")', $statements[0]);
+ }
+
+ public function testAddingSpatialIndex()
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('The database driver in use does not support spatial indexes.');
+
+ $blueprint = new Blueprint('geo');
+ $blueprint->spatialIndex('coordinates');
+ $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ }
+
+ public function testAddingFluentSpatialIndex()
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('The database driver in use does not support spatial indexes.');
+
+ $blueprint = new Blueprint('geo');
+ $blueprint->point('coordinates')->spatialIndex();
+ $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ }
+
+ public function testAddingRawIndex()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->rawIndex('(function(column))', 'raw_index');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('create index "raw_index" on "users" ((function(column)))', $statements[0]);
+ }
+
+ public function testAddingIncrementingID()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->increments('id');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "id" integer primary key autoincrement not null', $statements[0]);
+ }
+
+ public function testAddingSmallIncrementingID()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->smallIncrements('id');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "id" integer primary key autoincrement not null', $statements[0]);
+ }
+
+ public function testAddingMediumIncrementingID()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->mediumIncrements('id');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "id" integer primary key autoincrement not null', $statements[0]);
+ }
+
+ public function testAddingID()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->id();
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "id" integer primary key autoincrement not null', $statements[0]);
+
+ $blueprint = new Blueprint('users');
+ $blueprint->id('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" integer primary key autoincrement not null', $statements[0]);
+ }
+
+ public function testAddingBigIncrementingID()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->bigIncrements('id');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "id" integer primary key autoincrement not null', $statements[0]);
+ }
+
+ public function testAddingString()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->string('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" varchar not null', $statements[0]);
+
+ $blueprint = new Blueprint('users');
+ $blueprint->string('foo', 100);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" varchar not null', $statements[0]);
+
+ $blueprint = new Blueprint('users');
+ $blueprint->string('foo', 100)->nullable()->default('bar');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" varchar default \'bar\'', $statements[0]);
+ }
+
+ public function testAddingText()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->text('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" text not null', $statements[0]);
+ }
+
+ public function testAddingBigInteger()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->bigInteger('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" integer not null', $statements[0]);
+
+ $blueprint = new Blueprint('users');
+ $blueprint->bigInteger('foo', true);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" integer primary key autoincrement not null', $statements[0]);
+ }
+
+ public function testAddingInteger()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->integer('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" integer not null', $statements[0]);
+
+ $blueprint = new Blueprint('users');
+ $blueprint->integer('foo', true);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" integer primary key autoincrement not null', $statements[0]);
+ }
+
+ public function testAddingMediumInteger()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->mediumInteger('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" integer not null', $statements[0]);
+
+ $blueprint = new Blueprint('users');
+ $blueprint->mediumInteger('foo', true);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" integer primary key autoincrement not null', $statements[0]);
+ }
+
+ public function testAddingTinyInteger()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->tinyInteger('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" integer not null', $statements[0]);
+
+ $blueprint = new Blueprint('users');
+ $blueprint->tinyInteger('foo', true);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" integer primary key autoincrement not null', $statements[0]);
+ }
+
+ public function testAddingSmallInteger()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->smallInteger('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" integer not null', $statements[0]);
+
+ $blueprint = new Blueprint('users');
+ $blueprint->smallInteger('foo', true);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" integer primary key autoincrement not null', $statements[0]);
+ }
+
+ public function testAddingFloat()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->float('foo', 5, 2);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" float not null', $statements[0]);
+ }
+
+ public function testAddingDouble()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->double('foo', 15, 8);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" float not null', $statements[0]);
+ }
+
+ public function testAddingDecimal()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->decimal('foo', 5, 2);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" numeric not null', $statements[0]);
+ }
+
+ public function testAddingBoolean()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->boolean('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" tinyint(1) not null', $statements[0]);
+ }
+
+ public function testAddingEnum()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->enum('role', ['member', 'admin']);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "role" varchar check ("role" in (\'member\', \'admin\')) not null', $statements[0]);
+ }
+
+ public function testAddingJson()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->json('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" text not null', $statements[0]);
+ }
+
+ public function testAddingJsonb()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->jsonb('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" text not null', $statements[0]);
+ }
+
+ public function testAddingDate()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->date('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" date not null', $statements[0]);
+ }
+
+ public function testAddingYear()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->year('birth_year');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "birth_year" integer not null', $statements[0]);
+ }
+
+ public function testAddingDateTime()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->dateTime('created_at');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "created_at" datetime not null', $statements[0]);
+ }
+
+ public function testAddingDateTimeWithPrecision()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->dateTime('created_at', 1);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "created_at" datetime not null', $statements[0]);
+ }
+
+ public function testAddingDateTimeTz()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->dateTimeTz('created_at');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "created_at" datetime not null', $statements[0]);
+ }
+
+ public function testAddingDateTimeTzWithPrecision()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->dateTimeTz('created_at', 1);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "created_at" datetime not null', $statements[0]);
+ }
+
+ public function testAddingTime()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->time('created_at');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "created_at" time not null', $statements[0]);
+ }
+
+ public function testAddingTimeWithPrecision()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->time('created_at', 1);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "created_at" time not null', $statements[0]);
+ }
+
+ public function testAddingTimeTz()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->timeTz('created_at');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "created_at" time not null', $statements[0]);
+ }
+
+ public function testAddingTimeTzWithPrecision()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->timeTz('created_at', 1);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "created_at" time not null', $statements[0]);
+ }
+
+ public function testAddingTimestamp()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->timestamp('created_at');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "created_at" datetime not null', $statements[0]);
+ }
+
+ public function testAddingTimestampWithPrecision()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->timestamp('created_at', 1);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "created_at" datetime not null', $statements[0]);
+ }
+
+ public function testAddingTimestampTz()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->timestampTz('created_at');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "created_at" datetime not null', $statements[0]);
+ }
+
+ public function testAddingTimestampTzWithPrecision()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->timestampTz('created_at', 1);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "created_at" datetime not null', $statements[0]);
+ }
+
+ public function testAddingTimestamps()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->timestamps();
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(2, $statements);
+ $this->assertEquals([
+ 'alter table "users" add column "created_at" datetime',
+ 'alter table "users" add column "updated_at" datetime',
+ ], $statements);
+ }
+
+ public function testAddingTimestampsTz()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->timestampsTz();
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+ $this->assertCount(2, $statements);
+ $this->assertEquals([
+ 'alter table "users" add column "created_at" datetime',
+ 'alter table "users" add column "updated_at" datetime',
+ ], $statements);
+ }
+
+ public function testAddingRememberToken()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->rememberToken();
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "remember_token" varchar', $statements[0]);
+ }
+
+ public function testAddingBinary()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->binary('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" blob not null', $statements[0]);
+ }
+
+ public function testAddingUuid()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->uuid('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" varchar not null', $statements[0]);
+ }
+
+ public function testAddingForeignUuid()
+ {
+ $blueprint = new Blueprint('users');
+ $foreignUuid = $blueprint->foreignUuid('foo');
+ $blueprint->foreignUuid('company_id')->constrained();
+ $blueprint->foreignUuid('laravel_idea_id')->constrained();
+ $blueprint->foreignUuid('team_id')->references('id')->on('teams');
+ $blueprint->foreignUuid('team_column_id')->constrained('teams');
+
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertInstanceOf(ForeignIdColumnDefinition::class, $foreignUuid);
+ // because string return type in Hyperf\Database\Schema\Grammars\Grammar::compileForeign is a must
+ // it has a redundancy statement
+ $this->assertSame(9, count($statements));
+ $this->assertSame('alter table "users" add column "foo" varchar not null', $statements[0]);
+ $this->assertSame('alter table "users" add column "company_id" varchar not null', $statements[1]);
+ $this->assertSame('alter table "users" add column "laravel_idea_id" varchar not null', $statements[2]);
+ $this->assertSame('alter table "users" add column "team_id" varchar not null', $statements[3]);
+ $this->assertSame('alter table "users" add column "team_column_id" varchar not null', $statements[4]);
+ }
+
+ public function testAddingIpAddress()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->ipAddress('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" varchar not null', $statements[0]);
+ }
+
+ public function testAddingMacAddress()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->macAddress('foo');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "users" add column "foo" varchar not null', $statements[0]);
+ }
+
+ public function testAddingGeometry()
+ {
+ $blueprint = new Blueprint('geo');
+ $blueprint->geometry('coordinates');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "geo" add column "coordinates" geometry not null', $statements[0]);
+ }
+
+ public function testAddingPoint()
+ {
+ $blueprint = new Blueprint('geo');
+ $blueprint->point('coordinates');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "geo" add column "coordinates" point not null', $statements[0]);
+ }
+
+ public function testAddingLineString()
+ {
+ $blueprint = new Blueprint('geo');
+ $blueprint->linestring('coordinates');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "geo" add column "coordinates" linestring not null', $statements[0]);
+ }
+
+ public function testAddingPolygon()
+ {
+ $blueprint = new Blueprint('geo');
+ $blueprint->polygon('coordinates');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "geo" add column "coordinates" polygon not null', $statements[0]);
+ }
+
+ public function testAddingGeometryCollection()
+ {
+ $blueprint = new Blueprint('geo');
+ $blueprint->geometrycollection('coordinates');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "geo" add column "coordinates" geometrycollection not null', $statements[0]);
+ }
+
+ public function testAddingMultiPoint()
+ {
+ $blueprint = new Blueprint('geo');
+ $blueprint->multipoint('coordinates');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "geo" add column "coordinates" multipoint not null', $statements[0]);
+ }
+
+ public function testAddingMultiLineString()
+ {
+ $blueprint = new Blueprint('geo');
+ $blueprint->multilinestring('coordinates');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "geo" add column "coordinates" multilinestring not null', $statements[0]);
+ }
+
+ public function testAddingMultiPolygon()
+ {
+ $blueprint = new Blueprint('geo');
+ $blueprint->multipolygon('coordinates');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('alter table "geo" add column "coordinates" multipolygon not null', $statements[0]);
+ }
+
+ public function testAddingGeneratedColumn()
+ {
+ $blueprint = new Blueprint('products');
+ $blueprint->create();
+ $blueprint->integer('price');
+ $blueprint->integer('discounted_virtual')->virtualAs('"price" - 5');
+ $blueprint->integer('discounted_stored')->storedAs('"price" - 5');
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('create table "products" ("price" integer not null, "discounted_virtual" integer as ("price" - 5), "discounted_stored" integer as ("price" - 5) stored)', $statements[0]);
+
+ $blueprint = new Blueprint('products');
+ $blueprint->integer('price');
+ $blueprint->integer('discounted_virtual')->virtualAs('"price" - 5')->nullable(false);
+ $blueprint->integer('discounted_stored')->storedAs('"price" - 5')->nullable(false);
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(2, $statements);
+ $expected = [
+ 'alter table "products" add column "price" integer not null',
+ 'alter table "products" add column "discounted_virtual" integer not null as ("price" - 5)',
+ ];
+ $this->assertSame($expected, $statements);
+ }
+
+ public function testGrammarsAreMacroable()
+ {
+ // compileReplace macro.
+ $this->getGrammar()::macro('compileReplace', function () {
+ return true;
+ });
+
+ $c = $this->getGrammar()::compileReplace();
+
+ $this->assertTrue($c);
+ }
+
+ public function testCreateTableWithVirtualAsColumn()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->create();
+ $blueprint->string('my_column');
+ $blueprint->string('my_other_column')->virtualAs('my_column');
+
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('create table "users" ("my_column" varchar not null, "my_other_column" varchar as (my_column))', $statements[0]);
+
+ $blueprint = new Blueprint('users');
+ $blueprint->create();
+ $blueprint->string('my_json_column');
+ $blueprint->string('my_other_column')->virtualAsJson('my_json_column->some_attribute');
+
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('create table "users" ("my_json_column" varchar not null, "my_other_column" varchar not null as (json_extract("my_json_column", \'$."some_attribute"\')))', $statements[0]);
+
+ $blueprint = new Blueprint('users');
+ $blueprint->create();
+ $blueprint->string('my_json_column');
+ $blueprint->string('my_other_column')->virtualAsJson('my_json_column->some_attribute->nested');
+
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('create table "users" ("my_json_column" varchar not null, "my_other_column" varchar not null as (json_extract("my_json_column", \'$."some_attribute"."nested"\')))', $statements[0]);
+ }
+
+ public function testCreateTableWithVirtualAsColumnWhenJsonColumnHasArrayKey()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->create();
+ $blueprint->string('my_json_column')->virtualAsJson('my_json_column->foo[0][1]');
+
+ $conn = $this->getConnection();
+ $conn->shouldReceive('getConfig')->andReturn(null);
+
+ $statements = $blueprint->toSql($conn, $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame("create table \"users\" (\"my_json_column\" varchar not null as (json_extract(\"my_json_column\", '$.\"foo\"[0][1]')))", $statements[0]);
+ }
+
+ public function testCreateTableWithStoredAsColumn()
+ {
+ $blueprint = new Blueprint('users');
+ $blueprint->create();
+ $blueprint->string('my_column');
+ $blueprint->string('my_other_column')->storedAs('my_column');
+
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('create table "users" ("my_column" varchar not null, "my_other_column" varchar as (my_column) stored)', $statements[0]);
+
+ $blueprint = new Blueprint('users');
+ $blueprint->create();
+ $blueprint->string('my_json_column');
+ $blueprint->string('my_other_column')->storedAsJson('my_json_column->some_attribute');
+
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('create table "users" ("my_json_column" varchar not null, "my_other_column" varchar not null as (json_extract("my_json_column", \'$."some_attribute"\')) stored)', $statements[0]);
+
+ $blueprint = new Blueprint('users');
+ $blueprint->create();
+ $blueprint->string('my_json_column');
+ $blueprint->string('my_other_column')->storedAsJson('my_json_column->some_attribute->nested');
+
+ $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar());
+
+ $this->assertCount(1, $statements);
+ $this->assertSame('create table "users" ("my_json_column" varchar not null, "my_other_column" varchar not null as (json_extract("my_json_column", \'$."some_attribute"."nested"\')) stored)', $statements[0]);
+ }
+
+ public function getGrammar()
+ {
+ return new SQLiteGrammar();
+ }
+
+ protected function getConnection()
+ {
+ return m::mock(Connection::class);
+ }
+}