Giter Club home page Giter Club logo

clickhousebuilder's Introduction

Clickhouse Query Builder

Build Status Coverage Status

Requirements

php 7.1+

Install

Via composer

composer require the-tinderbox/clickhouse-builder

Usage

For working query builder we must previously instantiate and pass in constructor the-tinderbox/clickhouse-php-client.

$server = new Tinderbox\Clickhouse\Server('127.0.0.1', '8123', 'default', 'user', 'pass');
$serverProvider = (new Tinderbox\Clickhouse\ServerProvider())->addServer($server);

$client = new Tinderbox\Clickhouse\Client($serverProvider);
$builder = new Builder($client);

After that we can build and perform sql queries.

Select columns

$builder->select('column', 'column2', 'column3 as alias');
$builder->select(['column', 'column2', 'column3 as alias']);
$builder->select(['column', 'column2', 'column3' => 'alias']);

All this calls will be transformed into next sql:

SELECT `column`, `column2`, `column3` AS `alias`

Also, as a column we can pass closure. In this case in closure will be passed instance of Column class, inside which we can setup column how we want. This can be useful for difficult expressions with many functions, subqueries and etc.

$builder->select(function ($column) {
    $column->name('time')->sumIf('time', '>', 10);
});

Will be compiled in:

SELECT sumIf(`time`, time > 10)
$builder->select(function ($column) {
    $column->as('alias') //or ->name('alias') in this case
    ->query()
    ->select('column')
    ->from('table');
});

Will be compiled in:

SELECT  (SELECT `column` FROM `table) as `alias`

Same behavior can be also achieved by any of the following approaches:

$1 = $builder->select(function ($column) {
         $column->as('alias') //or ->name('alias') in this case
            ->query(function ($query) {
                $query->select('column')->from('table');
            })
});
$2 = $builder->select(function ($column) {
         $column->as('alias') //or ->name('alias') in this case
            ->query($builder->select('column')->from('table'));
});

Notice! Functions on columns is not stable and under development.

From

$builder->select('column')->from('table', 'alias');

Produce the following query:

SELECT `column` FROM `table` as `alias`

Also can be passed closure or builder as argument for performing sub query.

$builder->from(function ($from) {
    $from->query()->select('column')->from('table');
});
SELECT * FROM (SELECT `column` FROM `table`)

or

$builder->from(function ($from) {
    $from->query(function ($query) {
        $query->select('column')->from('table');
    });
});

or

$builder->from(function ($from) {
    $from->query($builder->select('column')->from('table'));
});

or

$builder->from($builder->select('column')->from('table'));

It is all variants of the same sql query which was listed above.

Sample coefficient

$builder->select('column')->from('table')->sample(0.1);
SELECT `column` FROM `table` SAMPLE 0.1

I think there no need for additional words)

Joins

$builder->from('table')->join('another_table', 'any', 'left', ['column1', 'column2'], true, 'alias');
SELECT * FROM `table` GLOBAL ANY LEFT JOIN `another_table` AS `alias` USING `column1`, `column2`

For performing subquery as first argument you can pass closure or builder.

$builder->from('table')->join(function ($join) {
    $join->query()->select('column1', 'column2')->from('table2');
}, 'any', 'left', ['column1', 'column2']);

$builder->from('table')->join($builder->select('column1', 'column2')->from('table2'), 'any', 'left', ['column1', 'column2']);
SELECT * FROM `table` ANY LEFT JOIN (SELECT `column1`, `column2` FROM `table2`) USING `column1`, `column2`

Also there are many helper functions with hardcoded arguments, like strict or type and they combinations.

$builder->from('table')->anyLeftJoin('table', ['column']);
$builder->from('table')->allLeftJoin('table', ['column']);
$builder->from('table')->allInnerJoin('table', ['column']);
$builder->from('table')->anyInnerJoin('table', ['column']);

$buulder->from('table')->leftJoin('table', 'any', ['column']);
$buulder->from('table')->innerJoin('table', 'all', ['column']);

You can use array join as well.

$builder->from('test')->arrayJoin('someArr');
$builder->from('test')->leftArrayJoin('someArr');
SELECT * FROM `test` ARRAY JOIN `someArr`
SELECT * FROM `test` LEFT ARRAY JOIN `someArr`

Temporary tables usage

There are some cases when you need to filter f.e. users by their ids, but amount of ids is huge. You can store users ids in local file, upload it to server and use it as temporary table.

Read more about local files here in section Using local files.

Select

You should pass instance of TempTable with declared table structure to attach file to query.

$builder->addFile(new TempTable('numbersTable', 'numbers.tsv', ['number' => 'UInt64'], Format::TSV));

$builder->table(raw('numbers(0,1000)')->whereIn('number', 'numbersTable')->get();

If you want tables to be detected automatically, call addFile method before calling whereIn.

You can use local files in whereIn, prewhereIn, havingIn and join statements of query builder.

Insert

If you want to insert file or files into Clickhouse, you could use insertFile and insertFiles methods.

$builder->table('test')->insertFile(['date', 'userId'], 'test.tsv', Format::TSV);

Or you can pass batch of files into insertFiles method and all of them will be inserted asynchronously.

$builder->table('test')-insertFiles(['date', 'userId'], [
    'test-1.tsv',
    'test-2.tsv',
    'test-3.tsv',
    'test-4.tsv',
    'test-5.tsv',
    'test-6.tsv',
    'test-7.tsv',
], Format::TSV)

Also, you can use helper and insert data to temporary table with engine Memory.

$builder->table('test')->values('test.tsv')->format(Format::TSV);

into_memory_table($builder, [
    'date' => 'Date',
    'userId' => 'UInt64'
]);

Helper will drop temporary table with name test and creates table with declared structure, engine Memory and inserts data from test.tsv file into just created table.

It's helpful if you want to fill some table with data to execute query and then drop it.

Prewhere, where, having

All example will be about where, but same behavior also is for prewhere and having.

$builder->from('table')->where('column', '=', 'value');
$builder->from('table')->where('column', 'value');
SELECT * FROM `table` WHERE `column` = 'value'

All string values will be wrapped with single quotes. If operator is not provided = will be used. If operator is not provided and value is an array, then IN will be used.

$builder->from('table')->where(function ($query) {
    $query->where('column1', 'value')->where('column2', 'value');
});
SELECT * FROM `table` WHERE (`column1` = 'value' AND `column2` = 'value')

If in the first argument was passed closure, then all wheres statements from inside will be wrapped with parenthesis. But if on that builder (inside closure) will be specified from then it will be transformed into subquery.

$builder->from('table')->where(function ($query) {
    $query->select('column')->from('table');
})
SELECT * FROM `table` WHERE (SELECT `column` FROM `table`)

Almost same is for value parameter, except wrapping into parenthesis. Any closure or builder instance passed as value will be converted into subquery.

$builder->from('table')->where('column', 'IN', function ($query) {
    $query->select('column')->from('table');
});
SELECT * FROM `table` WHERE `column` IN (SELECT `column` FROM `table`)

Also you can pass internal representation of this statement and it will be used. I will no talk about this with deeper explanation because its not preferable way to use this.

Like joins there are many helpers with hardcoded parameters.

$builder->where();
$builder->orWhere();

$builder->whereRaw();
$builer->orWhereRaw();

$builder->whereIn();
$builder->orWhereIn();

$builder->whereGlobalIn();
$builder->orWhereGlobalIn();

$builder->whereGlobalNotIn();
$builder->orWhereGlobalNotIn();

$builder->whereNotIn();
$builder->orWhereNotIn();

$builder->whereBetween();
$builder->orWhereBetween();

$builder->whereNotBetween();
$builder->orWhereNotBetween();

$builder->whereBetweenColumns();
$builder->orWhereBetweenColumns();

$builder->whereNotBetweenColumns();
$builder->orWhereNotBetweenColumns();

Also there is method to make where by dictionary:

$builder->whereDict('dict', 'attribute', 'key', '=', 'value');
SELECT dictGetString('dict', 'attribute', 'key') as `attribute` WHERE `attribute` = 'value'

If you want to use complex key, you may pass an array as $key, then array will be converted to tuple. By default all strings will be escaped by single quotes, but you may pass an Identifier instance to pass for example column name:

$builder->whereDict('dict', 'attribute', [new Identifier('column'), 'string value'], '=', 'value');

Will produce:

SELECT dictGetString('dict', 'attribute', tuple(`column`, 'string value')) as `attribute` WHERE `attribute` = 'value'

Group By

Works like select.

$builder->from('table')->select('column', raw('count()'))->groupBy('attribute');

Final query will be like:

SELECT `column`, count() FROM `table` GROUP BY `attribute`

Order By

$builder->from('table')->orderBy('column', 'asc', 'fr');

In the example above, third argument is optional

SELECT *  FROM `table` ORDER BY `column` ASC COLLATE 'fr'

Aliases:

$builder->orderByAsc('column');
$builder->orderByDesc('column');

For column there are same behaviour like in select method.

Limit

There are two types of limit. Limit and limit n by.

Limit n by:

$builder->from('table')->limitBy(1, 'column1', 'column2');

Will produce:

SELECT * FROM `table` LIMIT 1 BY `column1`, `column2`

Simple limit:

$builder->from('table')->limit(10, 100);

Will produce:

SELECT * FROM `table` LIMIT 100, 10

Union ALL

In unionAll method can be passed closure or builder instance. In case of closure inside will be passed builder instance.

$builder->from('table')->unionAll(function($query) {
    $query->select('column1')->from('table');
})->unionAll($builder->select('column2')->from('table'));
SELECT * FROM `table` UNION ALL SELECT `column1` FROM `table` UNION ALL SELECT `column2` FROM `table`

Performing request and getting result.

After building request you must call get() method for sending request to the server. Also there has opportunity to make asynchronous requests. Its works almost like unionAll.

$builder->from('table')->asyncWithQuery(function($query) {
    $query->from('table');
});
$builder->from('table')->asyncWithQuery($builder->from('table'));
$builder->from('table')->asyncWithQuery()->from('table');

This callings will produce the same behavior. Two queries which will be executed asynchronous. Now, if you call get() method, as result will be returned array, where numeric index correspond to the result of request with this number.

Integrations

Laravel or Lumen < 5.5

You can use this builder in Laravel/Lumen applications.

Laravel

In config/app.php add:

    'providers' => [
        ...
        \Tinderbox\ClickhouseBuilder\Integrations\Laravel\ClickhouseServiceProvider::class,
        ...
    ]

Lumen

In bootstrap/app.php add:

$app->register(\Tinderbox\ClickhouseBuilder\Integrations\Laravel\ClickhouseServiceProvider::class);

Connection configures via config/database.php.

Example with alone server:

'connections' => [
    'clickhouse' => [
        'driver' => 'clickhouse',
        'host' => 'ch-00.domain.com',
        'port' => '',
        'database' => '',
        'username' => '',
        'password' => '',
        'options' => [
            'timeout' => 10,
            'protocol' => 'https'
        ]
    ]
]

Get a new builder:

DB::connection('clickhouse')->query();

or

'connections' => [
    'clickhouse' => [
        'driver' => 'clickhouse',
        'servers' => [
            [
                'host' => 'ch-00.domain.com',
                'port' => '',
                'database' => '',
                'username' => '',
                'password' => '',
                'options' => [
                    'timeout' => 10,
                    'protocol' => 'https'
                ]
            ],

            [
                'host' => 'ch-01.domain.com',
                'port' => '',
                'database' => '',
                'username' => '',
                'password' => '',
                'options' => [
                    'timeout' => 10,
                    'protocol' => 'https'
                ]
            ]
        ]
    ]
]

Example with cluster:

'connections' => [
    'clickhouse' => [
        'driver' => 'clickhouse',
        'clusters' => [
            'cluster-name' => [
                [
                    'host' => '',
                    'port' => '',
                    'database' => '',
                    'username' => '',
                    'password' => '',
                    'options' => [
                        'timeout' => 10,
                        'protocol' => 'https'
                    ]
                ],

                [
                    'host' => '',
                    'port' => '',
                    'database' => '',
                    'username' => '',
                    'password' => '',
                    'options' => [
                        'timeout' => 10,
                        'protocol' => 'https'
                    ]
                ]
            ]
        ]
    ]
]

Example with server with tag:

'connections' => [
    'clickhouse' => [
        'driver' => 'clickhouse',
        'servers' => [
            [
                'host' => 'ch-00.domain.com',
                'port' => '',
                'database' => '',
                'username' => '',
                'password' => '',
                'options' => [
                    'timeout' => 10,
                    'protocol' => 'https',
                    'tags' => [
                        'tag'
                    ],
                ],
            ],
            [
                'host' => 'ch-01.domain.com',
                'port' => '',
                'database' => '',
                'username' => '',
                'password' => '',
                'options' => [
                    'timeout' => 10,
                    'protocol' => 'https'
                ],
            ],
        ],
    ],
]

Choose server without cluster:

DB::connection('clickhouse')->using('ch-01.domain.com')->select(...);

Or execute each new query on random server:

DB::connection('clickhouse')->usingRandomServer()->select(...);

Choose cluster:

DB::connection('clickhouse')->onCluster('test')->select(...);

Use server with tag:

DB::connection('clickhouse')->usingServerWithTag('tag')->select(...);

You can use both servers and clusters config directives and choose on which server query should be executed via onCluster and using methods. If you want to choose server outside cluster, you should just call onCluster(null) and then call using method. You can call usingRandomServer and using methods with selected cluster or not.

clickhousebuilder's People

Contributors

agolovenkin avatar astro2049 avatar dikopylov avatar evsign avatar facedsid avatar gitlog avatar leo108 avatar pavemaksim avatar rez1dent3 avatar romanpravda avatar suvorovis avatar tachigami avatar tianhe1986 avatar vitalcrazz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

clickhousebuilder's Issues

limitBy with offset

Please add offset to limitBy and takeBy, this is one of the frequently used functions for me :)
thanks for the nice clickhouse package

Set session_id parameter

Is it possible to set the session_id parameter?
Doesn't work with options array

'options' => [
    'session_id' => "my_session_key"
]

Move authentication url params to header

sometimes I have an error and log throw with authentication. I don't want the password to appear in the log file.

I see package https://github.com/smi2/phpClickHouse has AUTH_METHOD_HEADER
Is this package has a function to support it?

[2021-06-10 08:58:13] production.ERROR: cURL error 7: Failed to connect to clickhouse-host: Connection refused (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for https://clickhouse-host?wait_end_of_query=1&database=database&user=user&password=password{"exception":"[object] (GuzzleHttp\\Exception\\ConnectException(code: 0): cURL error 7: Failed to connect to clickhouse-host: Connection refused (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for https://clickhouse-host?wait_end_of_query=1&database=database&user=user&password=password at /var/www/vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php:210)

timeout config didn't work?

I've check code in \Tinderbox\ClickhouseBuilder\Integrations\Laravel\Connection::assembleServer
I didn't find the code to set timeout

Uint64 in where casts to string

I'm using Uint64 in my where clause. In php I should use string for this type. In your wrap method is_string check goes before is_numeric. And all queries makes slower with string equality.
For example:
WHERE item_id = "1241214"
much slower than
WHERE item_id = 1241214

Support for offset

Hi,

Great work on the code base, its difficult to find any laravel implementations with clickhouse.

Would it be possible to support the Builder method offset?

EG:

DB::table()->where()->limit()->offset()->get();

Laravel supports this by default in their query builder

Method Tinderbox\ClickhouseBuilder\Integrations\Laravel\Builder::offset does not exist.

Thanks

count return string instead of number

Could you please explain if I run the query:
DB::connection('clickhouse')->select(raw('select count(*) as summary from table'));
I get the result like this
array:1 [
0 => array:1 [
"summary" => "4309900"
]
]

Why "summary" get "4309900" as string, not a number ?

Testing is failed

Hi!

$ composer test

There were 2 errors:

  1. Tinderbox\ClickhouseBuilder\LaravelIntegrationTest::test_connection_select_async
    array_combine(): Both parameters should have an equal number of elements

ClickhouseBuilder/src/Integrations/Laravel/Connection.php:331
ClickhouseBuilder/tests/LaravelIntegrationTest.php:229

  1. Tinderbox\ClickhouseBuilder\LaravelIntegrationTest::test_builder_async_get
    array_combine(): Both parameters should have an equal number of elements

ClickhouseBuilder/src/Integrations/Laravel/Connection.php:331
ClickhouseBuilder/src/Integrations/Laravel/Builder.php:44
ClickhouseBuilder/tests/LaravelIntegrationTest.php:358

ERRORS!
Tests: 91, Assertions: 360, Errors: 2.

join query error

$builder->from('table')->join(function ($query) {
    $query->select('column1', 'column2')->from('table2');
}, 'any', 'left', ['column1', 'column2']);

The above code execution error!!!

Fatal error: Uncaught Error: Call to undefined method Tinderbox\ClickhouseBuilder\Query\JoinClause::select() in /Users/jiao/ProjectPhp/test/TestCk/app/1.php:25

Create/Drop table on cluster

Hello!

Now, where I want create table on cluster with this code:

$this->builder
            ->onCluster('test')
            ->createTableIfNotExists('test', 'ReplicatedMergeTree ORDER BY number', [
                'number' => 'UInt64',
            ]);

I have error, because suffix ON CLUSTER test can't added.

This also applies to the DROP TABLE query.

Your composer requirement for now crash your library

In your composer.json you have "the-tinderbox/clickhouse-php-client": "^1.0" which now update to 1.0.12 (minor!!!) version and require 4 parameters (before was 3) in this class:

the-tinderbox/clickhouse-php-client/src/Query/QueryStatistic.php on line 56

So, maybe you update your library for new code of library or bind version of this requirement library to 1.0.10?

Package's tap function is not compatible with laravel's tap function

This package's tap function (https://github.com/the-tinderbox/ClickhouseBuilder/blob/master/src/functions.php#L13) is not compatible with Laravel's tap function (https://github.com/laravel/framework/blob/9.x/src/Illuminate/Support/helpers.php#L300) which causes errors in Laravel if the tap function from this package is registered before Laravel's version (The inverse works fine since this package always sends a callback to the function)

Error example:

ArgumentCountError 

  Too few arguments to function tap(), 1 passed in /application/vendor/laravel/framework/src/Illuminate/Support/Facades/Storage.php on line 74 and exactly 2 expected

  at vendor/the-tinderbox/clickhouse-builder/src/functions.php:13
      9▕      * @param callable $callback
     10▕      *
     11▕      * @return mixed
     12▕      */
  ➜  13▕     function tap($value, $callback)
     14▕     {
     15▕         $callback($value);
     16▕ 
     17▕         return $value;

      +20 vendor frames 
  21  artisan:37
      Illuminate\Foundation\Console\Kernel::handle()

select query format

$builder->format() works only with formats in which each character in the name is in uppercase
Version 1.2.0

Laravel 6

"the-tinderbox/clickhouse-builder": "^2.3"

Declaration of Tinderbox\\ClickhouseBuilder\\Integrations\\Laravel\\Connection::table($table) must be compatible with Illuminate\\Database\\Connection::table($table, $as = NULL)

join query error

When trying to do

$builder->join(function (JoinClause $join) {
            $join->query()
                ->select('main_cpm')
                ->table('campaigns')
                ->on('campaigns.campaign_id', '=', 'user_event_agr.campaign_id');
        })

I get the following

   ` Call to undefined method PhpClickHouseLaravel\Builder::newQuery()`

INSERT Not working on laravel

Tinderbox\Clickhouse\Exceptions\TransportException : Host [] returned error: Code: 27, e.displayText() = DB::Exception: Cannot parse input: expected ( before: FORMAT JSON: (at row 2)

"laravel/framework": "5.7.*",
"php": "^7.1.3",

my try
DB::connection('clickhouse')->using('XXX')->select("INSERT INTO db.table (pid, price, timestamp) VALUES (1, 2, 3)");

I run this in tabix "INSERT INTO db.table (pid, price, timestamp) VALUES (1, 2, 3)" and it was executed successfully!

if i try
DB::connection('clickhouse')->using('XXX')->select("ISELECT pid, price, timestampFROM db.table"); and it was executed successfully but INSERT return error

set password and user in header

in http interface , the password can place in url with paramater

$ echo 'SELECT 1' | curl 'http://localhost:8123/?user=user&password=password' -d @-

however , the password and username will be exposed in log system when the log system will log the uri .
so , it should support login by place user and password in header instead of url

the example is blow
the user in url replace by X-ClickHouse-User in header and password in url replace by X-ClickHouse-Key in header

$ echo 'SELECT 1' | curl -H 'X-ClickHouse-User: user' -H 'X-ClickHouse-Key: password' 'http://localhost:8123/' -d @-

relative

Old ANY INNER|RIGHT|FULL JOINs are disabled by default

Clickhouse Server : v19.14.3.3

Error:

Old ANY INNER|RIGHT|FULL JOINs are disabled by default. Their logic would be changed. Old logic is many-to-one for all kinds of ANY JOINs. It's equil to apply distinct for right table keys. Default bahaviour is reserved for many-to-one LEFT JOIN, one-to-many RIGHT JOIN and one-to-one INNER JOIN. It would be equal to apply distinct for keys to right, left and both tables respectively. Set any_join_distinct_right_table_keys=1 to enable old bahaviour..

OLD: with ANY ( error )

SELECT 
	cart_id,
	product_id,
	added_time,
	paid_time
FROM 
	carts
ANY INNER JOIN 
	checkouts
USING 
	cart_id, product_id

NEW: without ANY ( success )

SELECT 
	cart_id,
	product_id,
	added_time,
	paid_time
FROM 
	carts
INNER JOIN 
	checkouts
USING 
	cart_id, product_id
...->join('table_name', '', 'inner', ['column'])...

EXCEPTION:

Value '' is not part of the enum Tinderbox\ClickhouseBuilder\Query\Enums\JoinStrict

PHP 8 support

PHP 8 is officially released. It will be very nice to fix php version for this package.
I believe there will be no backward compatibility issues...
Thanks!

Add a licence

I would like to fork and modify this repo to better suit my needs but seems like I can not legaly modify and use your code without any open source licence. Can you please licence this repo or make it clear that you are not offering ony license?

https://choosealicense.com/no-permission/

How to use aggregate function in `SELECT`

I can't understand how to use aggregate function in select.

For example:

->select("sum(click_cnt) as click_cnt, zone_id")

or this

->select("sum(click_cnt) as click_cnt","zone_id")

converts to

`sum(click_cnt)` as `click_cnt`, `zone_id`

But i want:

converts to sum(`click_cnt`) as `click_cnt`, `zone_id`

How to solve this case?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.