Common R2DBC Resources
This repository contains common resources for R2DBC projects. These are in particular:
R2DBC H2 Implementation
License: Apache License 2.0
In embedded mode, it's common to keep the database open as long as the JVM runs, otherwise, the state is discarded if the last connection gets closed. It would be good to have builder shortcuts for .option("DB_CLOSE_DELAY=…")
in the sense of closeDelay(Duration)
and a command that sets .option("DB_CLOSE_DELAY=-1")
to keep the database until the VM exits.
Repository declarations in published poms tend to pollute user pom's by inheriting dependency repository declarations. We should move these out of the way into the JMH profile.
Based on r2dbc/r2dbc-spi#54
H2Statement.execute()
switches on whether or not an INSERT
is being issues as to whether it uses Session.update
vs. Session.update
.
private Flux<H2Result> execute(String sql) {
if (INSERT.matcher(this.sql).matches()) {
return this.client.update(sql, this.bindings.bindings)
.map(result -> H2Result.toResult(result.getGeneratedKeys(), result.getUpdateCount()));
} else {
return this.client.query(sql, this.bindings.bindings)
.map(result -> H2Result.toResult(result, null));
}
}
H2 throws an exception when query
is used without a SELECT
so this will fail for commands like CREATE TABLE
that exist in Spring Data R2dbc.
Hence, the criteria here should be on SELECT
.
H2 supports "$1" statements in SQL queries and uses positional binding.
Since the SPI Example test now supports multiple paradigms, migrate toward that example. (Or consider supporting BOTH versions!)
Currently, only the file
and mem
protocols are supported and one cannot connect to a locally running H2 instance. Would be cool if that worked.
I used the column label(alias) using "as" clause, it throws an IllegalArgumentException
.
Is specification this behavior?
e.g.
R2dbc r2dbc = new R2dbc(connectionFactory);
r2dbc.useHandle(h -> h.select("SELECT id as value FROM users ORDER BY id")
.mapResult(result -> result.map((row, rowMetadata) -> row.get("value"))
)).subscribe(System.out::println);
reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalArgumentException: Column name 'VALUE' does not exist in column names [ID]
Caused by: java.lang.IllegalArgumentException: Column name 'VALUE' does not exist in column names [ID]
at io.r2dbc.h2.H2Row.getColumn(H2Row.java:117)
at io.r2dbc.h2.H2Row.get(H2Row.java:72)
at io.r2dbc.spi.Row.get(Row.java:46)
...
I hope to use column label(alias).
Exceptions raised by H2 are DbException
(runtime exceptions). These are not properly translated into R2DBC exceptions.
Reproducer:
H2ConnectionFactory connectionFactory = new H2ConnectionFactory(H2ConnectionConfiguration
.builder() //
.inMemory("r2dbc") //
.username("sa") //
.password("") //
.option("DB_CLOSE_DELAY=-1").build());
Flux.from(connectionFactory.create())
.flatMap(h2Connection -> h2Connection
.createStatement("SELECT foobar FROM unknown").execute())
.blockLast();
Stack trace:
Exception in thread "main" org.h2.message.DbException: Tabelle "UNKNOWN" nicht gefunden
Table "UNKNOWN" not found [42102-199]
at org.h2.message.DbException.get(DbException.java:205)
at org.h2.message.DbException.get(DbException.java:181)
at org.h2.command.Parser.readTableOrView(Parser.java:7146)
at org.h2.command.Parser.readTableFilter(Parser.java:1895)
at org.h2.command.Parser.parseSelectSimpleFromPart(Parser.java:2641)
at org.h2.command.Parser.parseSelectSimple(Parser.java:2788)
at org.h2.command.Parser.parseSelectSub(Parser.java:2636)
at org.h2.command.Parser.parseSelectUnion(Parser.java:2469)
at org.h2.command.Parser.parseSelect(Parser.java:2440)
at org.h2.command.Parser.parsePrepared(Parser.java:814)
at org.h2.command.Parser.parse(Parser.java:788)
at org.h2.command.Parser.parse(Parser.java:760)
at org.h2.command.Parser.prepareCommand(Parser.java:683)
at org.h2.engine.Session.prepareLocal(Session.java:627)
at org.h2.engine.Session.prepareCommand(Session.java:565)
at io.r2dbc.h2.client.SessionClient.createCommand(SessionClient.java:117)
at io.r2dbc.h2.client.SessionClient.lambda$prepareCommand$3(SessionClient.java:93)
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:100)
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1575)
at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onComplete(FluxDefaultIfEmpty.java:100)
at reactor.core.publisher.Operators.complete(Operators.java:132)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:122)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:63)
at reactor.core.publisher.FluxDefaultIfEmpty.subscribe(FluxDefaultIfEmpty.java:42)
at reactor.core.publisher.FluxMap.subscribe(FluxMap.java:62)
at reactor.core.publisher.FluxPeek.subscribe(FluxPeek.java:83)
at reactor.core.publisher.FluxFlatMap.subscribe(FluxFlatMap.java:97)
at reactor.core.publisher.Flux.subscribe(Flux.java:7922)
at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:389)
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:121)
at reactor.core.publisher.FluxJust$WeakScalarSubscription.request(FluxJust.java:99)
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:162)
at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:335)
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:90)
at reactor.core.publisher.FluxJust.subscribe(FluxJust.java:70)
at reactor.core.publisher.FluxMapFuseable.subscribe(FluxMapFuseable.java:63)
at reactor.core.publisher.FluxFlatMap.subscribe(FluxFlatMap.java:97)
at reactor.core.publisher.Flux.subscribe(Flux.java:7922)
at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:389)
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114)
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2131)
at reactor.core.publisher.FluxMap$MapSubscriber.request(FluxMap.java:155)
at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:335)
at reactor.core.publisher.FluxMap$MapSubscriber.onSubscribe(FluxMap.java:86)
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.core.publisher.MonoMap.subscribe(MonoMap.java:55)
at reactor.core.publisher.FluxSourceMono.subscribe(FluxSourceMono.java:46)
at reactor.core.publisher.FluxFlatMap.subscribe(FluxFlatMap.java:97)
at reactor.core.publisher.Flux.blockLast(Flux.java:2389)
at org.springframework.data.r2dbc.testing.H2TestSupport.main(H2TestSupport.java:84)
Suppressed: java.lang.Exception: #block terminated with an error
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:93)
at reactor.core.publisher.Flux.blockLast(Flux.java:2390)
... 1 more
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Tabelle "UNKNOWN" nicht gefunden
Table "UNKNOWN" not found; SQL statement:
SELECT foobar FROM unknown [42102-199]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:451)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:427)
... 51 more
Should be:
R2dbcBadGrammarException
Looking at the code, the mock setup does not match how SessionClient
behaves.
Spec Update
Add Statement.returnGeneratedValues(String...)
Related to r2dbc/r2dbc-spi#28
We should add proper support for Java 8 date/time types, see https://github.com/r2dbc/r2dbc-spi/pull/51/files#diff-46abe2dfa890d75dfe2188cb02cc6d77R117 for guidance.
/cc @sdeleuze
H2ConnectionFactory.class
->
private static ConnectionInfo getConnectionInfo(H2ConnectionConfiguration configuration)
I don't understand , why you append :
to the end of builder!?
StringBuilder sb = new StringBuilder(START_URL).append(configuration.getUrl()).append(":");
So, i always get an error like this
Failed to obtain R2DBC Connection; nested exception is org.h2.message.DbException: A file path that is implicitly relative to the current working directory is not allowed in the database URL \"jdbc:h2:/~testdb:;USER=sa\". Use an absolute path, ~/name, ./name, or the baseDir setting instead. [90011-197]
jdbc:h2:/~testdb:
is not correct, but jdbc:h2:/~testdb
is correct
Implement H2's version of r2dbc/r2dbc-spi#22.
The current version does not seem to implement r2dbc-spi-1.0.0.M5's io.r2dbc.spi.Statement.executeReturningGeneratedKeys()
method. See the SPI feature for reference: r2dbc/r2dbc-spi#17
Comply with r2dbc/r2dbc-spi@3600c84
I created a small project to setup the database with liquibase and save some data to a db with spring data r2dbc. I tested this with postgres and it works like a charm. With H2 i get an empty Mono back from spring data, even though the data is written to the db. Here is a minimal test-setup in Kotlin with Spring Boot 2.1.2.RELEASE, Spring data r2dbc 1.0.0.M1 and r2dbc Arabba-M7.
@RunWith(SpringRunner::class)
@SpringBootTest
@AutoConfigureWebTestClient
class IntegrationTest {
private val logger = getLogger(IntegrationTest::class.java)
@Autowired
private lateinit var testRepo: TestRepository
@Test
fun `save data in h2 memory db`() {
val testentity = Testentity(null, "Some title", "me! or I!")
val savedEntity = testRepo.save(testentity)
StepVerifier.create(savedEntity)
// .expectNext(testentity.apply {id = 1}) // FIXME: I expect an entity to be returned
.verifyComplete()
StepVerifier.create(testRepo.findAll().collectList())
.consumeNextWith { entityCount -> logger.debug("There are ${entityCount.count()} test entities saved")}
.verifyComplete()
}
}
The needed code for the application setup is as follows:
Application Context:
@SpringBootApplication
@EnableR2dbcRepositories
class DatabaseConfig: AbstractR2dbcConfiguration() {
private val logger: Logger = getLogger(javaClass)
private var DB_UUID: String = "testdb"
@Bean
override fun connectionFactory(): ConnectionFactory {
val originDataSource = dataSource().unwrap(SimpleDriverDataSource::class.java)
val config = ConnectionFactories.get(ConnectionFactoryOptions.builder()
.option(ConnectionFactoryOptions.DRIVER, "h2")
.option(ConnectionFactoryOptions.PROTOCOL, "mem")
.option(ConnectionFactoryOptions.DATABASE, DB_UUID)
.option(ConnectionFactoryOptions.USER, originDataSource.username!!)
.option(ConnectionFactoryOptions.PASSWORD, originDataSource.password!!)
.build())
val queryFormatter = QueryExecutionInfoFormatter.showAll()
return ProxyConnectionFactory.builder(config)
// Log executed query information
.onAfterQuery{queryExecInfo -> queryExecInfo
.map(queryFormatter::format)
.map(logger::trace)
.subscribe()
}.build()
}
@Bean
fun converter(mappingContext: RelationalMappingContext, r2dbcCustomConversions: R2dbcCustomConversions): MappingR2dbcConverter {
return MappingR2dbcConverter(BasicRelationalConverter(mappingContext, r2dbcCustomConversions))
}
/**
* Enable SQL Schema generation via Liquibase. A dataSource is needed for this
*/
@Bean
fun dataSource(): DataSource = EmbeddedDatabaseBuilder()
.setName(DB_UUID)
.setType(EmbeddedDatabaseType.H2)
.ignoreFailedDrops(true)
.build()
}
Entity:
@Entity
data class Testentity(
@Id
@GeneratedValue(strategy = AUTO)
var id: Long?,
@NotNull
val title: String,
@NotNull
val creator: String
)
Repository:
@Repository
interface TestRepository : ReactiveCrudRepository<Testentity, Long>
Liquibase create table file:
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet id="1" author="numbernick">
<createTable tableName="testentity">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="title" type="varchar(500)"/>
<column name="creator" type="varchar(500)">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>
The output of the test shows that the save-methods works (it persists data), but does not return the saved data:
20-03-2019 13:06:39.346 [main] TRACE d.n.t.r.DatabaseConfig$$EnhancerBySpringCGLIB$$8f98f76c.invoke - Thread:main(1) Connection:1 Transaction:{Create:0 Rollback:0 Commit:0} Success:True Time:11 Type:Statement BatchSize:0 BindingsSize:1 Query:["INSERT INTO testentity (title, creator) VALUES($1, $2)"] Bindings:[(Some title,me! or I!)]
20-03-2019 13:06:39.366 [main] DEBUG d.n.test.reactiveh2.IntegrationTest.accept - There are 1 test entities saved
20-03-2019 13:06:39.366 [main] TRACE d.n.t.r.DatabaseConfig$$EnhancerBySpringCGLIB$$8f98f76c.invoke - Thread:main(1) Connection:2 Transaction:{Create:0 Rollback:0 Commit:0} Success:True Time:10 Type:Statement BatchSize:0 BindingsSize:0 Query:["SELECT id, title, creator FROM testentity"] Bindings:[]
This driver implementation should implement ConnectionFactory
discovery.
Unable to bind java.util.Date. Failure is:
java.lang.IllegalArgumentException: Cannot encode parameter of type java.util.Date
at io.r2dbc.h2.codecs.DefaultCodecs.encode(DefaultCodecs.java:80)
at io.r2dbc.h2.H2Statement.bind(H2Statement.java:75)
at io.r2dbc.h2.H2Statement.bind(H2Statement.java:38)
at org.springframework.data.r2dbc.dialect.IndexedBindMarkers$IndexedBindMarker.bind(IndexedBindMarkers.java:78)
at org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy$DefaultBindableInsert.bind(DefaultReactiveDataAccessStrategy.java:465)
at org.springframework.data.r2dbc.function.BindableOperation.bind(BindableOperation.java:53)
at org.springframework.data.r2dbc.function.DefaultDatabaseClient$DefaultTypedInsertSpec.lambda$exchange$2(DefaultDatabaseClient.java:996)
What is the recommended way to pass/bind java.util.Date?
IMO, the only option is the client of the Spring r2dbc H2 Repo to convert "somehow:)" manually? Or?
Setup:
CREATE TABLE IF NOT EXISTS PERSON (
ID BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
BIRTHDATE DATE
);
INSERT INTO Person(ID) VALUES (1);
Consuming the result of a SELECT * FROM PERSON
with row.get("birthdate")
fails with:
java.lang.NullPointerException: null
at io.r2dbc.h2.codecs.DateCodec.transform(DateCodec.java:59) ~[r2dbc-h2-0.8.0.M8.jar:na]
at io.r2dbc.h2.codecs.DateCodec.doDecode(DateCodec.java:44) ~[r2dbc-h2-0.8.0.M8.jar:na]
at io.r2dbc.h2.codecs.DateCodec.doDecode(DateCodec.java:31) ~[r2dbc-h2-0.8.0.M8.jar:na]
at io.r2dbc.h2.codecs.AbstractCodec.decode(AbstractCodec.java:60) ~[r2dbc-h2-0.8.0.M8.jar:na]
at io.r2dbc.h2.codecs.DefaultCodecs.decode(DefaultCodecs.java:73) ~[r2dbc-h2-0.8.0.M8.jar:na]
at io.r2dbc.h2.H2Row.get(H2Row.java:79) ~[r2dbc-h2-0.8.0.M8.jar:na]
at io.r2dbc.spi.Row.get(Row.java:63) ~[r2dbc-spi-0.8.0.M8.jar:na]
at org.springframework.data.r2dbc.convert.MappingR2dbcConverter$RowParameterValueProvider.getParameterValue(MappingR2dbcConverter.java:426) ~[spring-data-r2dbc-1.0.0.M2.jar:1.0.0.M2]
Right now, it is only possible to receive String as bind param:
r2dbc-h2/src/main/java/io/r2dbc/h2/H2Statement.java
Lines 68 to 73 in 84c0b91
This code accept as valid input:
bind("?1", 100)
bind("$1", 100)
We should support integers as bind params, like:
bind(0, 100)
getIndex
to support both approaches (
)Using r2bdc-h2-1.0.0.M7
I have to use $
rather than ?
(as README suggests) in order for select binding to function correctly.
For example:
public Mono<Policies> findServiceInstancePolicyById(String id) {
String index = "$1";
String selectServiceInstancePolicy = "select pk, id, description, from_datetime, from_duration, organization_whitelist from service_instance_policy where id = " + index;
List<ServiceInstancePolicy> serviceInstancePolicies = new ArrayList<>();
return
Flux
.from(client.execute().sql(selectServiceInstancePolicy)
.bind(index, id)
.map((row, metadata) ->
ServiceInstancePolicy
.builder()
.pk(row.get("pk", Long.class))
.id(row.get("id", String.class))
.description(row.get("description", String.class))
.fromDateTime(row.get("from_datetime", Timestamp.class) != null ? row.get("from_datetime", Timestamp.class).toLocalDateTime() : null)
.fromDuration(row.get("from_duration", String.class) != null ? Duration.parse(row.get("from_duration", String.class)): null)
.organizationWhiteList(row.get("organization_whitelist", String.class) != null ? new HashSet<String>(Arrays.asList(row.get("organization_whitelist", String.class).split("\\s*,\\s*"))): new HashSet<>())
.build())
.all())
.map(sp -> serviceInstancePolicies.add(sp))
.then(Mono.just(new Policies(Collections.emptyList(), serviceInstancePolicies)))
.flatMap(p -> p.isEmpty() ? Mono.empty(): Mono.just(p));
}
The project has adopted a no-author tag policy on all documentation. IDEs like to bring them in by default, so we need to clean them out.
When creating a table with a BLOB and a supporting entity class coupled with a ReactiveCrudRepository interface, the byte[] field will not save to the database. The resulting error is: java.lang.IllegalArgumentException: Cannot encode null parameter of type [Ljava.lang.Byte;)
Tracing into a unit test, the type of the binary field in converted to java.lang.Byte[], but the codec is looking for byte[] (i.e. type [b).
I've provided a simple project that can recreate the issue with a simple unit test.
Currently there is no support for spatial geometry type:
In the meantime, anyone having same requirement can:
CREATE TABLE theTable (
location GEOMETRY(POINT) NOT NULL,
);
final WKTReader wktReader = new WKTReader();
r2dbc.inTransaction(handle -> handle
.select("SELECT CAST(location AS VARCHAR) AS loc FROM theTable")
.mapRow(row -> row.get("loc", String.class))
.flatMap(loc -> Mono.create(sink -> {
try {
sink.success(wktReader.read(loc));
}
catch (Exception ex) {
sink.error(ex);
}
})));
When parsing bindings, the Stream api was fickle so I used a List operation.
private Flux<H2Result> execute(String sql) {
if (INSERT.matcher(this.sql).matches()) {
return this.client.update(sql, this.bindings.bindings)
.map(result -> H2Result.toResult(result.getGeneratedKeys(), result.getUpdateCount()));
} else {
return this.client.query(sql, this.bindings.bindings)
.map(result -> H2Result.toResult(result, null));
}
}
Since it's possible to have multiple types of statements chained together via ";", the check should be done against sql
, not this.sql
.
This type of leakage would be prevented if execute(sql)
was static.
Properly flag the README that this runs over some aspects of JDBC and is not 100% non blocking.
Instead, the purpose is to support testing and jump starting.
Currently the SessionClient enforces SessionRemote, are there any way to use the normal Session?
Current hasPendingTransaction API returns false all the time.
Project main page states "This driver provides the following features: Read and write support for all data types except LOB types (e.g. BLOB, CLOB)"
What is the plan to support for org.h2.value.ValueLobDb?
My use-case is JPA_H2_Repo writes java.Map as CLOB/ValueLobDb and then it's not possible to read it by R2DBC_H2_Repo.
Thanks!
Evaluate the changes in r2dbc/r2dbc-spi#42 for updates in this implementation.
Using r2dbc-h2-1.0.0.M7
I see that select bindings fail with
java.lang.NoSuchMethodError: org.h2.result.ResultInterface.getColumnType(I)I
at io.r2dbc.h2.H2ColumnMetadata.toColumnMetadata(H2ColumnMetadata.java:127) ~[r2dbc-h2-1.0.0.M7.jar:na]
at io.r2dbc.h2.H2RowMetadata.getColumnMetadatas(H2RowMetadata.java:106) ~[r2dbc-h2-1.0.0.M7.jar:na]
at io.r2dbc.h2.H2RowMetadata.toRowMetadata(H2RowMetadata.java:99) ~[r2dbc-h2-1.0.0.M7.jar:na]
at io.r2dbc.h2.H2Result.toResult(H2Result.java:86) ~[r2dbc-h2-1.0.0.M7.jar:na]
Use r2dbc-h2-0.8.0.M8
create table t(id integer primary key, v integer)
insert into t(id, v) values (1, null);
StepVerifier.create(
Mono.from(connectionFactory.create())
.flatMap(c -> Mono.from(c.createStatement("select * from t where id = 1").execute()))
.flatMap(result -> Mono.from(result.map((row, rowMetadata) -> row.get("v", Integer.class))))
).expectNext((Integer) null).verifyComplete();
Error:
expectation "expectNext(null)" failed (expected value: null; actual value: 0)
H2 support a lot of config options. Instead of being required to specify these via .option("CIPHER=AES")
and hoping no character harms the URL format, it would make sense to capture these via ConnectionFactoryOptions
and inspect the R2DBC url for these.
In H2ConnectionConfiguration
, we should introduce an options map that allows specifying these options. ConnectionInfo
accepts a Properties
object that can be used to notify H2 about these options.
See also: http://www.h2database.com/html/features.html#database_url
Support r2dbc/r2dbc-spi#23.
I just ran into an issue trying to call row.get(…, int.class)
with the error message saying int
is not a supported type. Switching to Integer.class
got it working but I wonder whether we should be able to decode primitives OOTB to avoid boxing.
We should provide a configuration property to set DB Close delay. Using H2 with Spring Data R2DBC opens/closes a connection each time we invoke an operation. Closing a in-memory connection causes that all previous state (tables, inserted data) is lost. The current workaround is using url("mem:test;DB_CLOSE_DELAY=10")
via H2ConnectionConfiguration.builder()
.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.