SQLite
SQLite é um mecanismo de banco de dados relacional de código aberto, escrito em C por D. Richard Hipp, em 2000. Ele não necessita da instalação de um servidor para o seu funcionamento, como ocorre com a maioria dos demais bancos de dados, acessando diretamente um arquivo em disco. O estado completo de uma base de dados fica armazenado nesse arquivo, geralmente com extensão arquivo.db
, .sqlite
ou .db3
. que pode ser utilizado diretamente em diversas plataformas. Ele não é um aplicativo independente e sim uma biblioteca que é incorporada junto com os aplicativos, sendo o mecanismo de banco de dados mais usado, servindo para armazenamento e manipulação de dados em navegadores da Web, sistemas operacionais, telefones celulares e outros sistemas integrados.
O SQLite usa uma sintaxe de SQL parecida com a do PostgreSQL, mas não impõe a verificação de tipo, tornando possível, por exemplo, se inserir uma string em uma coluna definida como inteiro. Como em outras versões de bancos de dados relacionais, o SQLite armazena dados em tabelas que podem conter campos de vários tipos de dados, como texto ou números inteiros. Essas tabelas podem ser acessadas (construídas, modificadas e consultadas) por meio de consultas SQL que realizam operações “CRUD” (criar, ler, atualizar, excluir). Um banco de dados SQLite pode ser criptografado usando o SQLite Encryption Extension (SEE) ou outra tecnologia, de forma a proteger dados de extrações não autorizadas.
Diversos Sistemas Gerenciadores de Bancos de Dados, SGBD, (em inglês RDBMS, Relational Database Management System) estão disponíveis para o SQLite, entre eles o SQLiteStudio, DB Browser, DBeaver. Também existem diversos plugins para navegadores e IDEs (como o VSCode).
SQLite no VSCode
O VSCode possui um plugin muito interessante para auxiliar no desenvolvimento com SQLite. Para usá-lo instale o plugin (extension) vscode-sqlite
. Os seguintes recursos ficam disponíveis:
- consulta a bancos de dados SQLite, visualização de tabelas, exportações de resultados para json, csv e html.
- uso da barra lateral (explorer) para listar bancos de dados, tabelas, colunas e views.
- preenchimento automático de palavras-chave, nomes de tabelas e colunas. Para isso deve-se vincular um banco de dados a um documento SQL, usando o comando: USE DATABASE.
- emissão de mensagens de erros amigáveis apontando o local do erro.
Existem comandos para criar novos arquivos sqlite, novas consultas, editar e executar scripts de consulta, visualização rápida de BD, fechar e removar um BD, atualizar, exibir resultados de consultas.
Para iniciar o uso crie um documento, por exemplo meuBD.db
. Clique nele (com botão direito) e gere um nova consulta. O arquivo é transformado automaticamente em um banco do SQLite. Também clicando com botão direito selecione “Open Database” para visualizar as tabelas e ter acesso a vários outros comandos. Consultas digitadas no editor podem ser executadas com as opções “Run Query” ou “Run Selected Query”.
Comandos SQLite
Existem diversas maneiras de inserir comandos SQL em um banco SQLite. Em um gerenciador de bancos de dados podemos abrir uma área de inserção de consultas e digitar, interativamente esses comandos. Alternativamente, se temos uma sequência de consultas válidas gravadas no arquivo consultas.sqljunto podemos executar no prompt de comando:
cat consultas.sql | sqlite3 test1.db # ou sqlite3 test2.db ".read consultas.sql" # ou sqlite3 test3.db < definicoes.sql # no windows sqlite3.exe test4.db ".read definicoes.sql"
Esses comandos produzem respectivamente os arquivos test_n.db após as execuções das consultas em definicoes.sql.
Projeto: vendas
Como uma ilustração de uso do SQLite vamos construir uma banco de dados para controle de uma loja, com tabelas para seus clientes, funcionários, fornecedores e itens vendidos. Todas as consultas SQL podem estar em um único arquivo .sql, mas nós as listaremos por partes para comentar os seus efeitos.
Consultas de definição das tabelas
O comando CREATE TABLE
serve para inserir uma nova tabela no banco de dados. IF NOT EXISTS
instrui a consulta a ser executada somente se a tabela ainda não foi criada. As definições de campo seguem dentro dos parênteses. O campo id INTEGER PRIMARY KEY AUTOINCREMENT
é um índice inteiro, usado como chave primária e de autoincremento. Campos definidos como NOT NULL não podem ser deixados nulos.
Primeiro criamos uma tabela no prompt de comando:
>> sqlite3 vendas.db
Em seguida criamos as tabelas. Para usar esse arquivo podemos usar os comandos listados acima ou abrir um gerenciador de bancos de dados e inserir as consultas nele. O uso do VSCode ou diretamente no código com o sqlite3 do python são exemplos desse uso.
-- criando a tabela clientes CREATE TABLE IF NOT EXISTS clientes ( id INTEGER PRIMARY KEY AUTOINCREMENT, nome VARCHAR(100) NOT NULL, sobrenome TEXT NOT NULL, email VARCHAR(100) NOT NULL, cidade TEXT NOT NULL, estado VARCHAR(2) NOT NULL, cep INTEGER NOT NULL ); -- criando a tabela de funcionarios CREATE TABLE IF NOT EXISTS funcionarios ( id INTEGER PRIMARY KEY AUTOINCREMENT, nome VARCHAR(100) NOT NULL, sobrenome TEXT NOT NULL, email TEXT NOT NULL, senha VARCHAR(20) NOT NULL, cargo VARCHAR(133), endereco TEXT NOT NULL, cidade VARCHAR(150) NOT NULL, estado VARCHAR(2) NOT NULL, cep INTEGER NOT NULL ); -- Criando a tabela fornecedores CREATE TABLE IF NOT EXISTS fornecedores ( id INTEGER PRIMARY KEY AUTOINCREMENT, nome VARCHAR(100) NOT NULL, cnpj VARCHAR(100) NOT NULL, email VARCHAR(100) NOT NULL, endereco TEXT NOT NULL, cidade VARCHAR(150) NOT NULL, estado VARCHAR(2) NOT NULL, cep INTEGER NOT NULL );
Após a execução desses consultas temos as tabelas clientes, funcionarios e fornecedores. Digamos que queremos alterar uma tabela para acrescentar, renomear ou excluir algum campo. ALTER TABLE
é a consulta usada.
-- para alterar uma tabela (alterando clientes) ALTER TABLE clientes ADD data_aniversario DATE; ALTER TABLE clientes ADD endereco TEXT NOT NULL; -- para renomear um campo ALTER TABLE clientes RENAME data_aniversario TO aniversario; -- para excluir um campo ALTER TABLE clientes DROP COLUMN aniversario;
Primeiro inserimos data_aniversario e endereco, depois renomeamos data_aniversario para aniversario, depois excluímos o campo aniversario. Da mesma forma tabelas inteiras podem ser excluídas com DROP TABLE
.
-- criar uma tabela para ser apagada depois CREATE TABLE IF NOT EXISTS usuarios ( id INTEGER PRIMARY KEY AUTOINCREMENT, nome VARCHAR(100) NOT NULL, codigo INTEGER NOT NULL ); -- excluir a tabela recém criada DROP TABLE usuarios;
Operações de apagamento devem ser executadas com cuidado pois os dados não podem ser recuperados!
Observe que no modelo adotado acima os cargos dos funcionários são inseridos como textos que devem ser repetidos para diversos funcionários com o mesmo cargo. Outra possibilidade consiste em ter uma tabela com os cargos em separado, referenciados na tabela funcionarios por meio de um id. Vamos fazer essas alterações.
-- criar a tabela cargos CREATE TABLE IF NOT EXISTS cargos ( id INTEGER PRIMARY KEY AUTOINCREMENT, descricao VARCHAR(200) NOT NULL ); -- exclui a coluna cargo de funcionarios ALTER TABLE funcionarios DROP COLUMN cargo; -- insere cargo_id ALTER TABLE funcionarios ADD COLUMN cargo_id INTEGER REFERENCES cargos(id);
O último comando falaria se não existisse a tabela referenciada cargos. O campo cargo_id é uma chave estrangeira (foreign key), ligado à tabela cargos, pelo seu campo id. Vamos aplicar o mesmo conceito na criação de chaves estrangeiras para produtos vendidos e vendas.
-- tabela produtos CREATE TABLE IF NOT EXISTS produtos ( id INTEGER PRIMARY KEY AUTOINCREMENT, fornecedor_id INTEGER, descricao VARCHAR(100), preco DECIMAL(10,2), FOREIGN KEY (fornecedor_id) REFERENCES fornecedores (id) ); -- tabela vendas CREATE TABLE IF NOT EXISTS vendas ( id INTEGER PRIMARY KEY AUTOINCREMENT, cliente_id INTEGER NOT NULL, funcionario_id INTEGER NOT NULL, data_venda DATETIME NOT NULL, total DECIMAL (10,2) NOT NULL, descricao TEXT, FOREIGN KEY (cliente_id) REFERENCES clientes (id), FOREIGN KEY (funcionario_id) REFERENCES funcionarios (id) ); -- tabela itens_vendas CREATE TABLE IF NOT EXISTS itens_vendas ( id INTEGER PRIMARY KEY AUTOINCREMENT, venda_id INTEGER NOT NULL, produto_id INTEGER NOT NULL, quantidade INTEGER NOT NULL, subtotal DECIMAL (10,2) NOT NULL, FOREIGN KEY (venda_id) REFERENCES vendas (id), FOREIGN KEY (produto_id) REFERENCES produto (id) );
Os relacionamentos entre as tabelas expressam o fato de que um funcionário pode fazer várias vendas, cada venda pode conter vários ítens de produtos, um único fornecedor pode ser responsável por vários produtos e mais de um funcionário podem ter o mesmo cargo.
Vemos que as, após todos esses passos, as tabelas possuem os seguintes relacionamentos:
Consultas de inserção e leitura de dados
Uma vez definidas as tabelas passamos a inserir dados. Faremos também consultas para verificar os dados inseridos. Dados são inseridos com INSERT INTO
. O nome da tabela e os campos são fornecidos e os valores a inserir. SELECT *
significa “selecione todos os campos”.
INSERT INTO clientes (nome, sobrenome, email, cidade, estado, cep, endereco) VALUES ('Caio', 'Zuretta', 'cz@hotmail.com', 'Seattle', 'WA', 123456789,'23rd Street AWE'); SELECT * FROM clientes; id nome sobrenome email cidade estado cep endereco 1 Caio Zuretta cz@hotmail.com Seattle WA 123456789 23rd Street AWE -- inserindo outro cliente INSERT INTO clientes (nome, sobrenome, email, endereco, cidade, estado, cep ) VALUES ('Polka', 'Brita', 'pbrita@gmail.com', 'Av. Contorno 432', 'Belo Horizonte', 'MG', 30876786); SELECT id, nome, sobrenome FROM clientes; id nome sobrenome 1 Caio Zuretta 2 Polka Brita -- várias linhas podem ser inseridas na mesma consulta INSERT INTO clientes (nome, sobrenome, email, endereco, cidade, estado, cep) VALUES ('Antonio', 'Tony', 'tmatador@gigamail.com.br', 'R. Pedro II, 34', 'Rio de Janeiro', 'RJ', 21654897), ('Martha', 'Maertis', 'marthis@onlymail.com', 'R. Joinha, 654', 'Goiania', 'GO', 41546546), ('Orlando', 'Orlandis', 'orlas@gmail.com', 'Av, Só que Não, 34', 'Itabira', 'MG', 35654654), ('Mirtes', 'Mello', 'mellom@gmail.com', 'SQL 123', 'Brasília', 'DF', 145428214); -- uma operação sobre todos as linhas de um campo UPDATE clientes SET email = UPPER(email); SELECT email FROM clientes WHERE nome="Mirtes"; email MELLOM@GMAIL.COM -- vamos retornar as minúsculas no email UPDATE clientes SET email = LOWER(email); SELECT email FROM clientes WHERE nome="Mirtes"; email mellom@gmail.com -- inserindo dados na tabela funcionarios INSERT INTO funcionarios (nome, sobrenome, email, senha, cargo_id, endereco, cidade, estado, cep) VALUES ('Pedro', 'Altusser', 'pedroalt@email.com', '123456', 1, 'R. João Paulo, 534', 'Rio de Janeiro', 'RJ', 21654897), ('Levindo', 'Lopes', 'lopest@gmail.com', '234456', 1, 'R. Paulo II, 534', 'Ardósias', 'RJ', 21115114), ('Silvana', 'Gomes', 'silvana@gmail.com', '344456', 2, 'R. Paulo I, 4', 'Ardósias', 'RJ', 21651145), ('Lucas', 'Sêtte', 'lucas@gmail.com', '3er456', 3, 'R. Bahia, 1355', 'Belo Horizonte', 'MG', 31454232); SELECT nome, email, cidade, estado, cep, cargo_id FROM funcionarios WHERE estado = "MG"; nome email cidade estado cep cargo_id Lucas lucas@gmail.com Belo Horizonte MG 31454232 3 -- inserindo cargos INSERT INTO cargos (descricao) VALUES ('Gerente'), ('Vendedor'), ('Desenvolvedor'); SELECT * FROM cargos; id descricao 1 Gerente 2 Vendedor 3 Desenvolvedor -- alterar o valor de um campo já inserido UPDATE funcionarios SET cargo_id=1 WHERE id=1; -- agora a tabela funcionarios está no estado SELECT * FROM funcionarios; id nome sobrenome email senha endereco cidade estado cep cargo_id 1 Pedro Altusser pedroalt@email.com 123456 R. João Paulo, 534 Rio de Janeiro RJ 21654897 3 2 Levindo Lopes lopest@gmail.com 234456 R. Paulo II, 534 Ardósias RJ 21115114 1 3 Silvana Gomes silvana@gmail.com 344456 R. Paulo I, 4 Ardósias RJ 21651145 2 4 Lucas Sêtte lucas@gmail.com 3er456 R. Bahia, 1355 Belo Horizonte MG 31454232 3
Uma vez preenchidas as tabelas podemos fazer consultas de todos os tipos. Para ler dados da tabela funcionarios com a descrição dos cargos em cargos usamos INNER JOIN
. As duas consultas abaixo são equivalentes:
-- INNER JOIN SELECT f.nome, c.descricao FROM funcionarios f INNER JOIN cargos c WHERE f.cargo_id = c.id; SELECT f.nome, c.descricao FROM funcionarios f INNER JOIN cargos c ON (f.cargo_id = c.id); nome descricao Pedro Desenvolvedor Levindo Gerente Silvana Vendedor Lucas Desenvolvedor -- aliases podem ser dados para qualquer campo. Strings são concatenados com || SELECT nome || " " || sobrenome as 'Funcionário' FROM funcionarios; Funcionário Pedro Altusser Levindo Lopes Silvana Gomes Lucas Sêtte
Aliases foram usados acima para atribuir nomes às tabelas (como em FROM funcionarios f INNER JOIN cargos c
) ou a compos resultados da consultas (como em nome || " " || sobrenome as 'Funcionário'
).
Consultas podem ser modificadas pelas condições em WHERE
, e partes dos campos podem ser encontrados com LIKE
. %
representa qualquer grupo de caracteres, _
(underline) significa um caracter.
-- Uma consulta simples com dupla condição SELECT id, nome || " " || sobrenome as 'Funcionário', estado FROM clientes WHERE estado = "MG" and id=2; id Funcionário estado 2 Polka Brita MG -- para apagar um ou mais registros (o registro listado acima) DELETE FROM clientes WHERE estado = "MG" and id=2; -- nome iniciado com "Ma" e a letra "a" no sobrenome SELECT id, nome, sobrenome FROM clientes WHERE nome LIKE 'Ma%' AND sobrenome LIKE '%a%'; id nome sobrenome 4 Martha Maertis SELECT id, nome, sobrenome FROM clientes WHERE nome LIKE '_a%'; id nome sobrenome 1 Caio Zuretta 4 Martha Maertis SELECT id, nome, sobrenome FROM clientes WHERE nome LIKE '_a___a'; id nome sobrenome 4 Martha Maertis
Para fazer outras consultas cruzadas, em mais de uma tabela, vamos entrar dados nas tabelas fornecedores e produtos.
-- inserindo fornecedores e produtos INSERT INTO fornecedores (nome, cnpj, email, endereco, cidade, estado, cep) VALUES ('Microsoft', '234.456-098', 'ms@ms.com', 'R. Pedro Alves, 34', 'Sorocaba', 'SP', 1234567), ('Apple', '212.1226-128', 'apps@apps.com', 'R. Gerino Silva, 456', 'Brasília', 'DF', 61256767), ('Lenovo', '2456.1567-676', 'lenovo@lenovo.com', 'R. Power Guido, 786', 'Manaus', 'AM', 23452345), ('Dell', '222.453-444', 'del@del.com', 'R. Vaga Errante, 13', 'Sorocaba', 'SP', 1234567), ('Logitec', '666.7777-888', 'logi@log.com', 'R. Ulva Gods Silva, 90', 'Brasília', 'DF', 61256767), ('Multilaser', '1111.9999-888', 'miltila@multi.com', 'R. Volvo Zona, 76', 'Itabira', 'MG', 3114045); INSERT INTO produtos (fornecedor_id, descricao, preco) VALUES (2, 'iPAD', 12345.80), (1, 'Windows 11', 67.90), (5, 'Teclado sem fio', 99.00), (3, 'Notebook Intel', 1560.00), (15, 'Mouse Chines', 13.33), (16, 'Chingling Roteador', 59.89); SELECT * FROM fornecedores; SELECT * FROM produtos; -- as tabelas resultado estão nas imagens abaixo
Com essas definições de valores vamos fazer algumas consultas para exibir a sintaxe de consultas JOIN
. JOIN
e INNER JOIN
são idênticos.
SELECT p.id, f.nome, p.descricao, p.preco FROM produtos p JOIN fornecedores f ON (p.fornecedor_id = f.id); id nome descricao preco 1 Apple iPAD 12345.8 2 Microsoft Windows 11 67.9 3 Logitec Teclado sem fio 99 4 Lenovo Notebook Intel 1560 SELECT p.id 'COD.', f.nome 'Vendedor', p.descricao 'Produto' , p.preco 'Preço' FROM produtos p RIGHT JOIN fornecedores f ON (p.fornecedor_id = f.id); COD. Vendedor Produto Preço 1 Apple iPAD 12345.8 2 Microsoft Windows 11 67.9 3 Logitec Teclado sem fio 99 4 Lenovo Notebook Intel 1560 NULL Dell NULL NULL NULL Multilaser NULL NULL
Suponha que desejamos fazer um relatório com todas as vendas para o cliente de sobrenome “Arbinger”. Podemos primeiro encontrar o id do cliente e, daí, todas as vendas para ele. Fazemos um JOIN para encontrar o nome dos funcionários que fizeram as vendas.
-- o id do cliente é SELECT id FROM clientes WHERE sobrenome="Arbinger"; 3 -- as vendas desse cliente SELECT * FROM vendas WHERE cliente_id = (SELECT id FROM clientes WHERE sobrenome="Arbinger"); -- as vendas associadas ao funcionário vendedor SELECT v.data_venda, v.total, v.descricao, f.nome, f.sobrenome FROM vendas v JOIN funcionarios f WHERE f.id = v.funcionario_id and v.cliente_id = (SELECT id FROM clientes WHERE sobrenome="Arbinger"); data_venda total descricao nome sobrenome 10/12/2020 200 Filial 1 Levindo Lopes 11/12/2020 100 Filial 1 Levindo Lopes 13/12/2020 280 Filial 2 Pedro Altusser 17/12/2020 290 Filial 4 Levindo Lopes
Se, além disso quisermos saber quais os itens foram vendidos fazemos:
SELECT v.data_venda, v.total, v.descricao, f.nome, f.sobrenome, p.descricao as "Produto" FROM vendas v JOIN funcionarios f JOIN itens_vendas i JOIN produtos p WHERE f.id = v.funcionario_id and v.id = i.venda_id and i.produto_id = p.id and v.cliente_id = (SELECT id FROM clientes WHERE sobrenome="Arbinger"); data_venda total descricao nome sobrenome Produto 10/12/2020 200 Filial 1 Levindo Lopes Windows 11 10/12/2020 200 Filial 1 Levindo Lopes Teclado sem fio 11/12/2020 100 Filial 1 Levindo Lopes iPAD
Observe que dois itens foram listados relativos à venda do dia 10/12/2020 e um item para o dia 11/12. Nenhum item foi listado para as vendas dos dias 13 e 17/12.
sqlite3 e Python
O python traz um módulo instalado com a biblioteca padrão para integrar o SQLite com o código. Para usá-lo basta importar esse módulo chamado sqlite3.
import sqlite3 connection = sqlite3.connect("escola.db") cursor = connection.cursor() sql = """ CREATE TABLE IF NOT EXISTS alunos ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, nome TEXT, idade INTEGER ); """ cursor.execute(sql) sql = """ INSERT INTO alunos (nome, idade) VALUES ('Alice', 21), ('Letícia', 22), ('Vinicius', 23), ('Guilherme', 12), ('Marcos', 37), ('Pedro', 6), ('Wanda', 20), ('Aluísio', 10); """ cursor.execute(sql) connection.commit() dados = cursor.execute("SELECT * FROM alunos WHERE idade <=' 22") for linha in dados: print(linha[0], linha[1], linha[2]) 1 Alice 21 4 Guilherme 12 6 Pedro 6 7 Wanda 20 8 Aluísio 10
Com o comando connection = sqlite3.connect("escola.db")
o banco de dados é criado, se já não existe. Um cursor é um objeto usado para fazer a relação entre o código (e as queries) com o banco. O objeto dados é um iterável que retorna tuplas com os resultados da query.
Digamos que queremos fazer uma consulta ao banco de dados pre-existente phylos.db
, usado nos exercícios anteriores. Para isso estabelecemos uma nova conecção com esse banco de dados.
connection = sqlite3.connect("vendas.db") sql = """ SELECT v.data_venda, v.total, v.descricao, f.nome, f.sobrenome, p.descricao as "Produto" FROM vendas v JOIN funcionarios f JOIN itens_vendas i JOIN produtos p WHERE f.id = v.funcionario_id and v.id = i.venda_id and i.produto_id = p.id and v.cliente_id = (SELECT id FROM clientes WHERE sobrenome="Arbinger"); """ cursor = connection.cursor() data = cursor.execute(sql) for row in data: print(row) # o resultado é: ('10/12/2020', 200, 'Filial 1', 'Levindo', 'Lopes', 'Windows 11') ('10/12/2020', 200, 'Filial 1', 'Levindo', 'Lopes', 'Teclado sem fio') ('11/12/2020', 100, 'Filial 1', 'Levindo', 'Lopes', 'iPAD')
sqlite3 e pandas
Uma outra possibilidade interessante é a de integrar os dataframes do pandas com o SQLite. Para isso importamos o sqlite3
e o pandas
e estabelecemos uma conexão com o banco de dados. Um dataframe pode ser carregado diretamente com o resultado da consulta com pandas.read_sql(sql, connection)
.
import sqlite3 import pandas as pd connection = sqlite3.connect("vendas.db") sql = """ SELECT v.data_venda, v.total, v.descricao, f.nome, f.sobrenome FROM vendas v JOIN funcionarios f WHERE f.id = v.funcionario_id; """ cursor = connection.cursor() # apenas para ver o resultado da consulta fazemos: data = cursor.execute(sql) for row in data: print(row) # o resultado: ('10/12/2020', 200, 'Filial 1', 'Levindo', 'Lopes') ('11/12/2020', 100, 'Filial 1', 'Levindo', 'Lopes') ('12/12/2020', 120, 'Filial 2', 'Levindo', 'Lopes') ('13/12/2020', 280, 'Filial 2', 'Pedro', 'Altusser') ('17/12/2020', 290, 'Filial 4', 'Levindo', 'Lopes') df = pd.read_sql(sql, connection) print(df) # o resultado: data_venda total descricao nome sobrenome 0 10/12/2020 200 Filial 1 Levindo Lopes 1 11/12/2020 100 Filial 1 Levindo Lopes 2 12/12/2020 120 Filial 2 Levindo Lopes 3 13/12/2020 280 Filial 2 Pedro Altusser 4 17/12/2020 290 Filial 4 Levindo Lopes df.to_sql("tabela_2", connection)
O último comando insere uma tabela tabela_2 no banco de dados vendas.db com os campos e valores desse dataframe.
Bibliografia
Sobre SQLite
- SQLite.org: SQLite Home
- SimpliLearn: What is SQLite
- Tutorialspoint: SQLite Tutorial
- SQLiteTutorial.net: SQLite Tutorial
- GeeksForGeeks: Intoduction to SQLite
Sobre o SQLite3, no Python
- Python Docs: SQLite3
- Tutorialspoint: SQLite – Python
- GeeksforGeeks: Python SQLite