Skip to content

Commit

Permalink
Merge branch 'master' into Laravel_0.9
Browse files Browse the repository at this point in the history
  • Loading branch information
harish81 committed Apr 14, 2023
2 parents 56df379 + da48833 commit a0c7248
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 24 deletions.
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,51 @@ You can install duckdb extensions too.
DB::connection('my_duckdb')
->select("SELECT * FROM read_csv_auto('s3://my-bucket/test-datasets/example1/us-gender-data-2022.csv') LIMIT 10")
```
### Writing a migration
```php
return new class extends Migration {
protected $connection = 'my_duckdb';
public function up(): void
{
DB::connection('my_duckdb')->statement('CREATE SEQUENCE people_sequence');
Schema::create('people', function (Blueprint $table) {
$table->id()->default(new \Illuminate\Database\Query\Expression("nextval('people_sequence')"));
$table->string('name');
$table->integer('age');
$table->integer('rank');
$table->timestamps();
});
}

public function down(): void
{
Schema::dropIfExists('people');
DB::connection('my_duckdb')->statement('DROP SEQUENCE people_sequence');
}
};
```

### Readonly Connection - A solution to concurrent query.
- in `database.php`
```php
'connections' => [
'my_duckdb' => [
'driver' => 'duckdb',
'cli_path' => env('DUCKDB_CLI_PATH', base_path('vendor/bin/duckdb')),
'cli_timeout' => 0,
'dbfile' => env('DUCKDB_DB_FILE', storage_path('app/duckdb/duck_main.db')),
'schema' => 'main',
'read_only' => true,
'pre_queries' => [
"SET s3_region='".env('AWS_DEFAULT_REGION')."'",
"SET s3_access_key_id='".env('AWS_ACCESS_KEY_ID')."'",
"SET s3_secret_access_key='".env('AWS_SECRET_ACCESS_KEY')."'",
],
'extensions' => ['httpfs', 'postgres_scanner'],
],
...
```


## Testing

Expand All @@ -104,6 +149,10 @@ DB::connection('my_duckdb')
composer test
```

## Limitations & FAQ

- https://duckdb.org/faq

## Changelog

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
Expand Down
14 changes: 10 additions & 4 deletions src/Commands/ConnectDuckdbCliCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

class ConnectDuckdbCliCommand extends Command
{
protected $signature = 'laravel-duckdb:connect {connection_name}';
protected $signature = 'laravel-duckdb:connect {connection_name} {--readonly=true}';

protected $description = 'Connect with duckdb cli to interactive query and development.';

Expand All @@ -16,11 +16,17 @@ class ConnectDuckdbCliCommand extends Command
public function handle(): void
{
$connection = config('database.connections.'.$this->argument('connection_name'));
$isReadonly = filter_var($this->option('readonly'), FILTER_VALIDATE_BOOLEAN);
if(!$connection || ($connection['driver']??'') !== 'duckdb') throw new \Exception("DuckDB connection named `".$this->argument('connection_name')."` not found!");

$cmd = $connection['cli_path']." ".$connection['dbfile'];
$this->info('Connecting to duckdb cli `'.$cmd.'`');
$this->process = Process::fromShellCommandline($cmd);
$cmd = [
$connection['cli_path'],
$connection['dbfile']
];
if($isReadonly) array_splice($cmd, 1, 0, '--readonly');

$this->info('Connecting to duckdb cli `'.implode(" ", $cmd).'`');
$this->process = new Process($cmd);
$this->process->setTimeout(0);
$this->process->setIdleTimeout(0);
$this->process->setTty(Process::isTtySupported());
Expand Down
34 changes: 19 additions & 15 deletions src/LaravelDuckdbConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ class LaravelDuckdbConnection extends PostgresConnection
private $installed_extensions = [];
public function __construct($config)
{
$this->database = $config['database'];
$this->config = $config;
$this->config['dbfile'] = $config['dbfile'];
//$this->setDatabaseName($this->config['dbfile']);

$this->useDefaultPostProcessor();
$this->useDefaultSchemaGrammar();
Expand Down Expand Up @@ -75,6 +75,7 @@ private function getDuckDBCommand($query, $bindings = [], $safeMode=false){
$this->config['cli_path'],
$this->config['dbfile'],
];
if($this->config['read_only']) array_splice($cmdParams, 1, 0, '--readonly');
if(!$safeMode) $cmdParams = array_merge($cmdParams, $preQueries);
$cmdParams = array_merge($cmdParams, [
"$escapeQuery",
Expand Down Expand Up @@ -103,7 +104,7 @@ private function installExtensions(){
}
if(!empty($sql)) Cache::forget($cacheKey);
foreach ($sql as $ext_name=>$sExtQuery) {
$this->statement($sExtQuery);
$this->executeDuckCliSql($sExtQuery, [], true);
}
$this->installed_extensions=$tobe_installed_extensions;
}
Expand All @@ -125,7 +126,6 @@ private function ensureDuckdbDirectory(){
private function executeDuckCliSql($sql, $bindings = [], $safeMode=false){

$command = $this->getDuckDBCommand($sql, $bindings, $safeMode);
//$process = Process::fromShellCommandline($command);
$process = new Process($command);
$process->setTimeout($this->config['cli_timeout']);
$process->setIdleTimeout(0);
Expand All @@ -145,32 +145,36 @@ private function executeDuckCliSql($sql, $bindings = [], $safeMode=false){
return json_decode($raw_output, true)??[];
}

public function statement($query, $bindings = [])
{
private function runQueryWithLog($query, $bindings=[]){
$start = microtime(true);

//execute
$this->executeDuckCliSql($query, $bindings);
$result = $this->executeDuckCliSql($query, $bindings);

$this->logQuery(
$query, [], $this->getElapsedTime($start)
);

return true;
return $result;
}

public function select($query, $bindings = [], $useReadPdo = true)
public function statement($query, $bindings = [])
{
$start = microtime(true);
$this->runQueryWithLog($query, $bindings);

//execute
$result = $this->executeDuckCliSql($query, $bindings);
return true;
}

$this->logQuery(
$query, [], $this->getElapsedTime($start)
);
public function select($query, $bindings = [], $useReadPdo = true)
{
return $this->runQueryWithLog($query, $bindings);
}

return $result;
public function affectingStatement($query, $bindings = [])
{
//for update/delete
//todo: we have to use : returning * to get list of affected rows; currently causing error;
return $this->runQueryWithLog($query, $bindings);
}

private function getDefaultQueryBuilder(){
Expand Down
5 changes: 5 additions & 0 deletions src/LaravelDuckdbServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ public function register(): void
'cli_path' => base_path('vendor/bin/duckdb'),
'cli_timeout' => 60,
'dbfile' => storage_path('app/duckdb/duck_main.db'),
//'database' => 'duck_main' //default to filename of dbfile, in most case no need to specify manually
'schema' => 'main',
'read_only' => false,
'pre_queries' => [],
'extensions' => []
];

$this->app->resolving('db', function ($db) use ($defaultConfig) {
$db->extend('duckdb', function ($config, $name) use ($defaultConfig) {
$defaultConfig['database'] = pathinfo($config['dbfile'], PATHINFO_FILENAME);

$config = array_merge($defaultConfig, $config);
$config['name'] = $name;
return new LaravelDuckdbConnection($config);
Expand Down
5 changes: 5 additions & 0 deletions src/Query/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ private function wrapFromClause($value, $prefixAlias = false){
}
return ($prefixAlias?$this->tablePrefix:'').$value;
}

public function compileTruncate(Builder $query)
{
return ['truncate '.$this->wrapTable($query->from) => []];
}
}
4 changes: 2 additions & 2 deletions src/Query/Processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace Harish\LaravelDuckdb\Query;

use Illuminate\Database\Query\Processors\Processor as BaseProcessor;
use Illuminate\Database\Query\Processors\PostgresProcessor;

class Processor extends BaseProcessor
class Processor extends PostgresProcessor
{

}
8 changes: 8 additions & 0 deletions src/Schema/Blueprint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Harish\LaravelDuckdb\Schema;

class Blueprint extends \Illuminate\Database\Schema\Blueprint
{

}
24 changes: 23 additions & 1 deletion src/Schema/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,29 @@

namespace Harish\LaravelDuckdb\Schema;

class Builder extends \Illuminate\Database\Schema\Builder
use Closure;
use Illuminate\Support\Facades\File;

class Builder extends \Illuminate\Database\Schema\PostgresBuilder
{
public static $alwaysUsesNativeSchemaOperationsIfPossible=true;
public function createDatabase($name)
{
return File::put($name, '') !== false;
}

public function dropDatabaseIfExists($name)
{
return File::exists($name)
? File::delete($name)
: true;
}

protected function createBlueprint($table, Closure $callback = null)
{
$this->blueprintResolver(function ($table, $callback, $prefix) {
return new Blueprint($table, $callback, $prefix);
});
return parent::createBlueprint($table, $callback);
}
}
18 changes: 17 additions & 1 deletion src/Schema/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,25 @@

namespace Harish\LaravelDuckdb\Schema;

use Illuminate\Database\Query\Grammars\PostgresGrammar;
use Illuminate\Database\Schema\Grammars\PostgresGrammar;
use Illuminate\Support\Fluent;

class Grammar extends PostgresGrammar
{
protected $transactions = false;

protected function typeInteger(Fluent $column)
{
return 'integer';
}

protected function typeBigInteger(Fluent $column)
{
return 'bigint';
}

protected function typeSmallInteger(Fluent $column)
{
return 'smallint';
}
}
1 change: 0 additions & 1 deletion tests/Feature/DuckDBBasicTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,5 @@ public function test_eloquent_model(){
public function test_query_exception(){
$this->expectException(QueryException::class);
$rs = DB::connection('my_duckdb')->selectOne('select * from non_existing_tbl01 where foo=1 limit 1');
dd($rs);
}
}
80 changes: 80 additions & 0 deletions tests/Feature/DuckDBSchemaStatementTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace Harish\LaravelDuckdb\Tests\Feature;

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Harish\LaravelDuckdb\Tests\TestCase;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Eloquent\Factories\Factory;

class PersonFactory extends Factory{

protected $model = Person::class;
public function definition()
{
return [
'name' => fake()->name(),
'age' => fake()->numberBetween(13, 50),
'rank' => fake()->numberBetween(1, 10),
'salary' => fake()->randomFloat(null, 10000, 90000)
];
}
}
class Person extends \Harish\LaravelDuckdb\LaravelDuckdbModel{
use \Illuminate\Database\Eloquent\Factories\HasFactory;
protected $connection = 'my_duckdb';
protected $table = 'people';
protected $guarded = ['id'];

protected static function newFactory()
{
return PersonFactory::new();
}
}
class DuckDBSchemaStatementTest extends TestCase
{
public function test_migration(){

Schema::connection('my_duckdb')->dropIfExists('people');
DB::connection('my_duckdb')->statement('DROP SEQUENCE IF EXISTS people_sequence');

DB::connection('my_duckdb')->statement('CREATE SEQUENCE people_sequence');
Schema::connection('my_duckdb')->create('people', function (Blueprint $table) {
$table->id()->default(new \Illuminate\Database\Query\Expression("nextval('people_sequence')"));
$table->string('name');
$table->integer('age');
$table->integer('rank');
$table->unsignedDecimal('salary')->nullable();
$table->timestamps();
});

$this->assertTrue(Schema::hasTable('people'));
}


public function test_model(){
//truncate
Person::truncate();

//create
$singlePerson = Person::factory()->make()->toArray();
$newPerson = Person::create($singlePerson);

//batch insert
$manyPersons = Person::factory()->count(10)->make()->toArray();
Person::insert($manyPersons);

//update
$personToUpdate = Person::where('id', $newPerson->id)->first();
$personToUpdate->name = 'Harish81';
$personToUpdate->save();
$this->assertSame(Person::where('name', 'Harish81')->count(), 1);

//delete
Person::where('name', 'Harish81')->delete();

//assertion count
$this->assertCount( 10, Person::all());
}
}
3 changes: 3 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@ protected function getEnvironmentSetUp($app)
'cli_timeout' => 0,
'dbfile' => '/tmp/duck_main.db',
]);

//default database just for schema testing, no need for production
$app['config']->set('database.default', 'my_duckdb');
}
}

0 comments on commit a0c7248

Please sign in to comment.