Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save marcelgsantos/223e7d2547e263f51dfc9469ac73c0a6 to your computer and use it in GitHub Desktop.

Select an option

Save marcelgsantos/223e7d2547e263f51dfc9469ac73c0a6 to your computer and use it in GitHub Desktop.
Clube do Livro - Craft & Code Club - Designing Data-Intensive Applications - Cap. 3 (parte 1) - Encontro 4

Clube do Livro - Designing Data-Intensive Applications - Cap. 3 (parte 1) - Encontro 4


Apresentação e Orientações Iniciais

O encontro foi realizado pelo Craft & Code Club, comunidade focada em engenharia de software e disciplinas relacionadas. Nelson Nobre apresentou brevemente a comunidade, destacando o site, o canal no YouTube com playlists de encontros anteriores, o blog com resumos e artigos e o servidor no Discord. O encontro contou com aproximadamente 21 participantes, incluindo membros novos e de outras comunidades. A gravação foi transmitida ao vivo no YouTube.

Orientações iniciais:

  • Evitar comentários negativos sobre empresas durante a gravação ao vivo.
  • Após desligar a live, a conversa fica totalmente livre.
  • O formato é de mesa redonda: todos são incentivados a falar e compartilhar experiências.
  • O capítulo discutido foi o Capítulo 3 do livro “Designing Data-Intensive Applications” de Martin Kleppmann.

Anotações

Modelos de Dados: Visão Geral

  • O modelo de dados é a parte mais importante do desenvolvimento de software porque afeta como pensamos na solução do problema. (Marcel)
  • Aplicações são camadas de modelos de dados construídas sobre outras camadas:
    • Camada do mundo real: pessoas, produtos, ações.
    • Camada de estruturas de dados: JSON, XML, registros, vértices, arestas.
    • Camada de armazenamento: memória, disco, rede.
    • Nível mais baixo: correntes elétricas, pulsos de luz, campos magnéticos.
  • Cada camada oculta a complexidade da camada anterior.
    • Ao lidar com JSON, não importa como o sistema operacional armazena aquela informação.
    • Ao usar banco de dados relacional com SQL, não importa como o banco guarda os dados internamente.
  • Os dados ficam, o comportamento da aplicação varia ao longo do tempo. (Nelson)
  • Os dados são o “petróleo” do sistema: toda aplicação pode mudar, mas os dados persistem.

Importância da Tipagem e Pensamento Estruturado

  • Quem começa com linguagens não tipadas (HTML, CSS, JavaScript) tende a não se preocupar com tipagem. (Joel Souza)
  • A chegada do TypeScript e de linguagens tipadas muda a forma de pensar sobre dados.
  • Ao se preocupar com tipos em linguagens de alto nível, os níveis mais baixos de abstração passam a fazer mais sentido.
  • Isso leva a uma preocupação crescente com ciência da computação: como as coisas são armazenadas e funcionam de baixo para cima.
  • A faculdade apresenta conceitos que só fazem sentido mais tarde na carreira. (Marcel)
  • O livro provoca esse resgate de conceitos deixados de lado ou não valorizados na época.

Experiências Pessoais com Modelos de Dados

  • Clebson Moura: trabalhou com equipes de análise de dados e não entendia os termos usados por eles. Após ler o capítulo, tudo fez sentido, inclusive mandou mensagem para um ex-colega de trabalho confirmando isso.
  • Wilson Neto: trabalhou em projeto analítico na InBev sem nunca ter trabalhado com analytics antes. A leitura do capítulo conectou os pontos. Percebeu que dado não normalizado nem sempre é melhor para performance: pode ser mais rápido para leitura, mas gera dor na escrita.
  • Marcel: reforçou que o sentimento de “as coisas fazendo sentido” é comum no clube do livro, citando o encontro anterior sobre OLTP e OLAP.

Natureza dos Dados e Organização

  • Os dados são uma matéria que, dependendo de como são modelados, servem a fins diferentes.
    • Dados organizados para operações transacionais: OLTP.
    • Dados organizados para análise: OLAP.
  • Analogia: objetos que você usa constantemente ficam na mesa; objetos raramente usados ficam no armário. O acesso ao dado deve seguir a mesma lógica. (Wilson)
  • Discussão em comunidade sobre quando usar banco de grafos: a maioria respondeu “vai de SQL”. Wilson alertou que isso pode gerar miopia, ignorando estruturas de dados que tornariam a vida da aplicação muito mais simples.

Linguagens Declarativas vs. Imperativas

  • Linguagem declarativa: você declara o que quer obter, as regras e as transformações. Não precisa dizer como o dado é obtido. (Marcel)
  • Linguagem imperativa: você especifica como obter os dados, quais algoritmos usar para busca e ordenação.
  • SQL é um exemplo de linguagem declarativa.
  • O motor do banco de dados possui um otimizador de consulta que decide a melhor estratégia de busca.
  • Vantagens da linguagem declarativa:
    • Sintaxe mais concisa.
    • Oculta detalhes de implementação de busca.
    • Se o banco implementa uma nova otimização, a consulta não precisa ser reescrita.
    • Em múltiplas CPUs ou máquinas, não é necessário se preocupar com a implementação.
  • Todos os bancos de dados, relacionais ou não, possuem algum mecanismo interno de otimização de consultas. (Marcel)
  • O SQL funciona como uma interface ou contrato: otimizações internas não afetam o cliente externo. (Nelson)
  • Um banco de dados é, no fundo, um conjunto de algoritmos e estruturas de dados super otimizados e abstraídos.
  • Bancos de documentos como MongoDB também possuem plano de otimização, como o pipeline de agregação (similar ao MapReduce usado no Spark). (Ullysses)
  • Redis, banco chave-valor não SQL, também possui otimização via índices.

Escolha do Banco de Dados Correto

  • Cada banco é preparado para uma atividade específica. Usar o banco errado é como usar uma bazuca para matar uma formiga. (Eduardo Bellinat)
  • Padrão simples para escolha: observar a chave estrangeira e se ela se repete.
    • Repete em um lado só: one-to-many, considerar banco de documentos.
    • Repete entre tabelas: many-to-many, considerar banco relacional ou de grafos. (Ramon Brito)
  • Bancos relacionais foram evoluindo para tentar atender lacunas (ex: armazenamento de binários), mas nem sempre são a bala de prata.
  • O que define a escolha não é só a normalização ou o design do banco, mas o problema que se quer resolver.
  • Em larga escala, usar o banco errado leva a débitos técnicos e tentativas de sustentar um design mal feito com infraestrutura.
  • Nem sempre se sabe qual o banco correto de início: isso se aprende com a vivência do software. (João Henrique)
  • Quando não se sabe bem qual será a dimensão das consultas, o banco relacional oferece mais flexibilidade. (Nelson)
  • Quando se sabe exatamente o padrão de acesso, o banco de documentos pode ser uma boa escolha.

Modelo Relacional: Histórico e Conceitos

  • Proposto por Edgar Codd em 1970.
  • Popularizado nos anos 1980.
  • Matematicamente organizado em relações (não tabelas) e tuplas (não registros).
  • O banco é chamado “relacional” porque a tabela em si é uma relação matemática, não apenas porque tabelas se relacionam entre si. (Wilson, com complemento de Marcel e Ullysses)
  • Fundamentado na álgebra relacional. (Ullysses)
  • Modelos alternativos da época (rede hierárquico) não tiveram o mesmo sucesso.
  • Modelos com objetos e XML surgiram depois, mas não se tornaram populares.
  • Por cerca de 30 anos (1980 a 2010), bancos relacionais dominaram o desenvolvimento.
  • Por volta de 2010, surgiu a onda NoSQL, com bancos de documentos (MongoDB, CouchDB) usando JSON como modelo mais flexível.
  • Bancos relacionais são dos mais complexos que existem: transações, níveis de isolamento, durabilidade. A maioria dos usuários usa apenas com configurações padrão. (Wilson)

Banco Relacional vs. Banco de Documentos

  • Banco relacional:
    • Dado centralizado: atualização em um único lugar.
    • Garante consistência via ACID (Atomicidade, Consistência, Isolamento, Durabilidade).
    • Transações entre tabelas garantem que ou tudo é salvo ou nada é salvo.
    • Consultas de múltiplas dimensões são mais flexíveis.
    • Muitos joins podem degradar a performance.
  • Banco de documentos:
    • Dado desnormalizado: informação duplicada em vários documentos.
    • Leitura mais rápida: não precisa de joins.
    • Manter consistência é mais trabalhoso: ao atualizar um valor, é necessário percorrer todos os documentos.
    • Consulta sempre parte pelo documento raiz.
    • Exemplo de David Retzlaff: tabela de usuário com região. No relacional, “Rio Grande do Sul” fica em uma tabela separada e é atualizado em um lugar. No banco de documentos, o valor pode estar duplicado e inconsistente (“RS” em um documento, “Rio Grande do Sul” em outro).
  • Bancos modernos estão convergindo: relacionais já suportam JSON e documentos; bancos de documentos já implementam transações.

Relações One-to-Many, Many-to-Many e Grafos

  • Relações one-to-many: bem atendidas por banco de documentos (dados aninhados, leitura em um acesso só).
  • Relações many-to-many: mais complexas.
    • No banco relacional: dado centralizado, joins necessários.
    • No banco de documentos: duplicidade de dados em todos os documentos relacionados.
    • Em escala muito grande, o banco relacional começa a “sangrar” em relações many-to-many.
    • Banco de grafos: ideal para relações complexas em rede, como redes sociais.
  • Exemplo clássico do livro: pessoa que nasceu em um lugar mas mora em outro. Em escala simples, o relacional resolve. Em escala grande, o banco de grafos responde muito mais rápido e de forma mais simples.
  • Exemplo de rede social: buscar as últimas 200 conexões de um usuário. No banco de documentos com dados aninhados, a busca pode ser difícil e custosa. O banco relacional pode ter caminho mais direto. (Marcel)
  • No banco relacional, é possível guardar informações já no índice (index covering), evitando acesso ao disco. (Ramon)

Normalização e Desnormalização

  • Normalização: dado em um único lugar, consistência garantida, mais joins necessários.
  • Desnormalização: dado duplicado, leitura mais rápida, consistência mais difícil de manter.
  • Nem sempre normalizar tudo é a melhor escolha. (Nelson)
    • Antigamente, o custo de armazenamento era caro, então normalizar fazia sentido.
    • Hoje, o custo de armazenamento é baixo. O custo de processamento para joins pode ser mais alto.
  • “Obsessão por normalização” pode ser um problema: nem sempre vale criar uma tabela separada para dados com baixa cardinalidade (ex: lista de estados). (Nelson)
  • Cardinalidade da variável é um critério útil para decidir normalizar ou não. (Ramon)
  • João Henrique: quando era estagiário, foi orientado a desnormalizar para melhorar performance. Parecia errado, mas era necessário para consultas mais eficientes.
  • O livro usa o conceito de “tudo é trade-off” do início ao fim.

Schema-on-Write vs. Schema-on-Read

  • Schema-on-write: esquema enforçado pelo banco no momento da escrita (bancos relacionais).
  • Schema-on-read: dados salvos com mais flexibilidade, esquema interpretado pela aplicação no momento da leitura (bancos de documentos).
  • Mesmo em bancos de documentos, a aplicação eventualmente vai usar os campos do dado (first_name, last_name), então o esquema existe implicitamente.
  • Em linguagens tipadas, o dado é convertido para o tipo da aplicação independentemente do banco.
  • A vantagem do schema-on-read: uma mudança no banco não quebra a aplicação imediatamente. A aplicação não explode, mas pode ter comportamento inesperado no código.

Impedância Objeto-Relacional (Object-Relational Impedance Mismatch)

  • Objetos na aplicação têm propriedades encapsuladas e comportamentos que alteram estado interno.
  • Banco relacional tem relações, tabelas e registros.
  • Essas duas estruturas não se sobrepõem: existe uma discrepância chamada impedância objeto-relacional.
  • O termo é emprestado da eletrônica.
  • ORMs (Object-Relational Mappers) fazem a tradução entre objetos e relações.

ORMs: Prós e Contras

  • Prós:
    • Abstração da comunicação com o banco: não é necessário escrever SQL bruto.
    • Facilita o desenvolvimento: interação com o banco via métodos da classe.
    • Ferramentas de migração de esquema integradas.
    • Mecanismo de cache out of the box.
    • Rastreamento de alterações no modelo e geração automática de queries de update.
    • Gerenciamento de transações (unit of work).
    • Construtores de queries (query builders).
    • Hidratação de objetos.
    • Exemplos: Django ORM (Python), Entity Framework (.NET), Hibernate (Java), TypeORM (Node.js).
  • Contras:
    • Não escondem totalmente a diferença entre os modelos: ainda é necessário conhecer SQL.
    • Projetados principalmente para aplicações OLTP, não para analytics.
    • Funcionam somente com bancos relacionais.
    • Podem gerar queries ineficientes se mal utilizados.
    • Problema do N+1: lazy loading mal configurado gera uma query para cada item de uma lista.
    • Podem tornar o desenvolvedor preguiçoso: objetos gigantes com objetos aninhados trazidos inteiramente para memória quando só uma parte era necessária.
  • Micro-ORMs como Dapper (.NET) fazem apenas o mapeamento, deixando o controle da query para o desenvolvedor. (Wilson)
  • Quando o ORM vira problema, geralmente o problema é o uso incorreto, não a ferramenta em si. (Eduardo)
  • Adicionar um índice simples no banco muitas vezes resolve o problema antes de qualquer mudança no ORM. (Ullysses)
  • Padrões de mapeamento: Active Record (objeto sabe como persistir) vs. Data Mapper (gerenciador central cuida da persistência).
  • Libertar-se do “database-centric thinking” (uma tabela = um objeto) é uma das maiores evoluções no uso de ORMs. (Marcel)
  • É possível ter um objeto de domínio que escreve em uma, duas ou três tabelas, ou em meia tabela.
  • Usar DTO / Read Model para leitura, sem precisar hidratar a entidade completa, melhora muito a performance.

DDD e Modelagem de Dados

  • O modelo de domínio (DDD) não precisa ser idêntico ao modelo de persistência no banco.
  • DDD estratégico: deve ser mantido intacto mesmo em aplicações de alta escala.
  • DDD tático: pode ser flexibilizado em prol de performance.
  • Em aplicações de alta escala (ex: 2000 requisições por segundo), o dado pode ser quebrado com base em performance, não em domínio. (Wilson)
  • Impedância objeto-relacional cruza com o conceito de DDD: modelo de entidades no domínio não precisa ser exatamente o que está no banco relacional.
  • CQRS (Command Query Responsibility Segregation) como estratégia: modelo de persistência normalizado para escrita, modelo desnormalizado para leitura.
  • Jéssica Nathany: migrando aplicação legada em VB com regras de negócio em stored procedures para microsserviços. O modelo de domínio mudou ao migrar de banco relacional para MongoDB, mas as regras de negócio não mudaram tanto.

CQRS e Dados Derivados

  • Dado derivado: versão do dado otimizada para leitura, derivada do dado de registro (fonte da verdade).
  • O livro menciona no Capítulo 1 o conceito de “sistema de registro” (fonte da verdade) vs. “dado derivado”.
  • CQRS não exige necessariamente dois bancos diferentes: pode ser implementado com um único banco SQL usando views materializadas ou tabelas separadas para leitura.
  • Exemplo de e-commerce: 80% leitura, 20% escrita. Preparar o banco para leituras rápidas reduz joins e melhora performance drasticamente. (Eduardo)
  • Materializar a view de leitura com menos joins (ex: de 12 tabelas para 2) resolve o problema sem precisar de NoSQL.
  • O autor conduz o leitor a desconstruir a tecnologia e pensar no padrão de leitura e escrita como a questão central. (Diogo)

Paradigma Orientado a Dados

  • Novo paradigma mencionado: em vez de carregar o objeto inteiro, verifica-se apenas as propriedades necessárias para aquela transformação específica.
  • O código cria as regras de negócio baseadas nas propriedades do dado, não no objeto como um todo.
  • Reduz o acoplamento entre a estrutura do banco e a estrutura da aplicação.
  • Não é totalmente novo: similar ao desenvolvimento code-first onde o código determina o banco.

Banco de Grafos

  • Ideal para dados com muitos relacionamentos complexos, como redes sociais.
  • Exemplo do livro: pessoa que nasceu em um lugar e mora em outro, em escala grande.
  • Linguagem de consulta para grafos é muito mais simples do que SQL recursivo para o mesmo problema.
  • O autor implementa o mesmo exemplo em SQL (com CTE recursiva, complexo) e em linguagem de grafos (muito mais simples).
  • Poucos profissionais trabalham diretamente com grafos no dia a dia, pois a maioria dos problemas não exige esse modelo.
  • Wilson: estudando a sintaxe Cypher por causa do encontro.
  • Grafos são mais comuns em empresas de redes sociais ou problemas específicos de relacionamentos em rede.

Ferramentas e Recursos Mencionados

  • Curso de Hussein Nasser sobre como bancos de dados funcionam por baixo dos panos (indicado por Ramon).
  • Livro “Fundamentos de Engenharia de Dados” (mencionado por Ullysses, lendo atualmente).
  • dbt (data build tool): ferramenta para linhagem de dados e criação de camada analítica com testes integrados.
  • Debezium: ferramenta de CDC (Change Data Capture) que lê logs do banco e alimenta outro banco em tempo real, rodando via Docker.
  • Snowflake: plataforma de dados mencionada por participantes que trabalham com analytics.
  • Spark / MapReduce: mencionado por Ullysses no contexto de processamento de dados e otimização de consultas.
  • Granola: ferramenta usada por Marcel para transcrever e organizar os encontros, gerando resumo dos tópicos e frases de impacto.

Considerações Finais do Encontro

  • O capítulo 3 é extenso e rico: a discussão foi encerrada antes de cobrir todos os tópicos.
  • Tópicos deixados para o próximo encontro: banco de grafos em profundidade, OLAP/analytics, dataframes, e demais conceitos do capítulo.
  • Marcel se comprometeu a trazer um pseudo-resumo dos tópicos restantes para guiar o próximo encontro.
  • Próximo encontro: segunda-feira seguinte, mesmo formato.

Insights

  • “O modelo de dado é a parte mais importante do desenvolvimento de software porque ele afeta como a gente pensa na solução do problema.” (Marcel, citando o livro)
  • “Os dados ficam, o comportamento da aplicação vai variando.” (Nelson): dados são o ativo permanente do sistema.
  • Cada camada de abstração oculta a camada anterior: ao escrever SQL, não importa como o banco armazena fisicamente. Isso é design intencional, não acidente.
  • Ao se preocupar com tipagem em linguagens de alto nível, os níveis mais baixos de abstração passam a fazer mais sentido. A tipagem é uma porta de entrada para pensar em ciência da computação. (Joel Souza)
  • O otimizador de consultas do banco funciona como o compilador de uma linguagem: faz otimizações antes de executar, independentemente do dialeto SQL ou NoSQL. (Marcel)
  • “Quando a ferramenta é o problema, não é a ferramenta que é o problema.” (Eduardo): o ORM mal usado é culpa do uso, não da ferramenta.
  • Dado não normalizado nem sempre é pior: para leitura analítica, desnormalizar pode ser a escolha correta. O erro é tratar normalização como dogma. (Wilson, João Henrique)
  • “Obsessão por normalização” pode ser um antipadrão moderno: o custo de armazenamento caiu drasticamente, mas o custo de processamento para joins permanece. (Nelson)
  • Libertar-se do “database-centric thinking” (uma tabela = um objeto) é uma das maiores evoluções no uso de ORMs. Um objeto de domínio pode escrever em múltiplas tabelas ou em meia tabela. (Marcel)
  • CQRS não exige dois bancos diferentes: pode ser implementado com um único banco SQL usando tabelas ou views materializadas separadas para leitura.
  • O autor do livro conduz o leitor a desconstruir tecnologias e chegar à pergunta central: qual é o padrão de leitura e escrita da sua aplicação? (Diogo)
  • “Você não precisa complicar na largada.” Começar simples (SQL, ORM), identificar os problemas reais de escala e só então evoluir para soluções mais complexas. (Eduardo)
  • “Quem nunca foi demitido por usar [banco relacional]?” (Artur, citando frase ouvida em empresas americanas): para casos genéricos, SQL é a escolha segura e pragmática.
  • Clube do livro como “quase uma aula de pós-graduação”: a combinação de leitura profunda com troca de experiências eleva o nível de maturidade de forma que a leitura solo não consegue. (Marcel)
  • Engenheiro de dados que aprende engenharia de software e vice-versa: as áreas convergem. Setenta por cento do que uma pós-graduação em dados ensina, um engenheiro de software já sabe. (Marcel)
  • O livro não se aprofunda em cada banco propositalmente: o objetivo é provocar o leitor a pensar melhor nos problemas, não ensinar cada tecnologia.

Palavras-Chaves

  • Modelo de dados
  • Camadas de abstração
  • Impedância objeto-relacional (Object-Relational Impedance Mismatch)
  • Banco de dados relacional
  • Banco de dados de documentos
  • Banco de dados de grafos
  • Banco de dados chave-valor
  • NoSQL
  • SQL
  • Álgebra relacional
  • Normalização
  • Desnormalização
  • Schema-on-write
  • Schema-on-read
  • OLTP (Online Transaction Processing)
  • OLAP (Online Analytical Processing)
  • ORM (Object-Relational Mapper)
  • Active Record
  • Data Mapper
  • Micro-ORM
  • Dapper
  • Entity Framework
  • Django ORM
  • TypeORM
  • Lazy Loading
  • N+1 Problem
  • Query Builder
  • Query Optimizer (Otimizador de Consultas)
  • Índice (Index)
  • Index Covering
  • Transação (ACID)
  • Atomicidade
  • Consistência
  • Isolamento
  • Durabilidade
  • CQRS (Command Query Responsibility Segregation)
  • View Materializada
  • Dado Derivado
  • Sistema de Registro (Source of Truth)
  • CDC (Change Data Capture)
  • ETL (Extract, Transform, Load)
  • Data Mesh
  • DDD (Domain-Driven Design)
  • DDD Estratégico
  • DDD Tático
  • Aggregate
  • Repository
  • DTO (Data Transfer Object)
  • Read Model
  • Database-centric thinking
  • Cardinalidade
  • Join
  • Many-to-many
  • One-to-many
  • Chave estrangeira
  • Tupla
  • Relação (matemática)
  • Edgar Codd
  • Formas Normais (Boyce-Codd)
  • MapReduce
  • Spark
  • Snowflake
  • Debezium
  • dbt (data build tool)
  • MongoDB
  • CouchDB
  • Redis
  • PostgreSQL
  • SQL Server
  • Cypher (linguagem de grafos)
  • CTE Recursiva (Common Table Expression)
  • Paradigma Orientado a Dados
  • Trade-off
  • Escala
  • Performance
  • Granola

Informações Finais

O quarto encontro do Clube do Livro do Craft & Code Club abordou o Capítulo 3 do livro “Designing Data-Intensive Applications” de Martin Kleppmann, com foco em modelos de dados. O encontro contou com aproximadamente 21 participantes, sendo o maior em número de participantes ativos até então.

A discussão começou com uma visão geral sobre a importância dos modelos de dados como a camada mais fundamental do desenvolvimento de software. O grupo explorou como as aplicações são construídas em camadas de abstração, desde o mundo real até os pulsos elétricos, e como cada camada oculta a complexidade da anterior. A conversa evoluiu para a importância da tipagem e do pensamento estruturado, com Joel Souza destacando como a adoção de TypeScript e linguagens tipadas muda a forma de pensar sobre dados e computação.

O grupo discutiu extensamente as diferenças entre bancos relacionais e bancos de documentos, incluindo os conceitos de normalização, desnormalização, schema-on-write e schema-on-read. Ficou claro que não existe uma escolha universalmente correta: tudo depende do padrão de acesso aos dados, da cardinalidade das variáveis e do problema que se quer resolver. Eduardo Bellinat provocou o grupo com a reflexão de que, quando uma ferramenta vira problema, geralmente o problema é o uso incorreto, não a ferramenta em si.

A discussão sobre ORMs foi rica e multifacetada, com participantes de diversas linguagens (Python, Java, C#, PHP, TypeScript) compartilhando experiências com Django ORM, Entity Framework, Hibernate e TypeORM. O grupo convergiu para a ideia de que ORMs são ferramentas poderosas quando bem usadas, mas exigem conhecimento do que acontece por baixo dos panos para evitar problemas como o N+1 e queries ineficientes.

O encontro também tocou em DDD, CQRS, dados derivados e banco de grafos, mas esses tópicos ficaram para aprofundamento no próximo encontro. Marcel se comprometeu a trazer um pseudo-resumo dos tópicos restantes do capítulo para guiar a próxima sessão, prevista para a segunda-feira seguinte.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment