nini-datacomp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. nini_datacomp-0.1.0/.gitattributes +4 -0
  2. nini_datacomp-0.1.0/.gitignore +23 -0
  3. nini_datacomp-0.1.0/.python-version +1 -0
  4. nini_datacomp-0.1.0/CHANGELOG.md +281 -0
  5. nini_datacomp-0.1.0/CMakeLists.txt +68 -0
  6. nini_datacomp-0.1.0/LICENSE +201 -0
  7. nini_datacomp-0.1.0/NOTICE +4 -0
  8. nini_datacomp-0.1.0/PKG-INFO +248 -0
  9. nini_datacomp-0.1.0/README.md +215 -0
  10. nini_datacomp-0.1.0/cpp/abi.cpp +25 -0
  11. nini_datacomp-0.1.0/cpp/batch.cpp +104 -0
  12. nini_datacomp-0.1.0/cpp/columns.hpp +155 -0
  13. nini_datacomp-0.1.0/cpp/common.hpp +28 -0
  14. nini_datacomp-0.1.0/cpp/compare.cpp +218 -0
  15. nini_datacomp-0.1.0/cpp/datacomp.h +56 -0
  16. nini_datacomp-0.1.0/cpp/keys.cpp +311 -0
  17. nini_datacomp-0.1.0/cpp/memory.cpp +143 -0
  18. nini_datacomp-0.1.0/cpp/parquet_meta.cpp +155 -0
  19. nini_datacomp-0.1.0/cpp/rowhash.cpp +174 -0
  20. nini_datacomp-0.1.0/pyproject.toml +155 -0
  21. nini_datacomp-0.1.0/src/nini_datacomp/__init__.py +55 -0
  22. nini_datacomp-0.1.0/src/nini_datacomp/_native.py +121 -0
  23. nini_datacomp-0.1.0/src/nini_datacomp/_signatures.py +27 -0
  24. nini_datacomp-0.1.0/src/nini_datacomp/api.py +250 -0
  25. nini_datacomp-0.1.0/src/nini_datacomp/core/__init__.py +1 -0
  26. nini_datacomp-0.1.0/src/nini_datacomp/core/allocator.py +60 -0
  27. nini_datacomp-0.1.0/src/nini_datacomp/core/arrow.py +207 -0
  28. nini_datacomp-0.1.0/src/nini_datacomp/core/arrow_c.py +374 -0
  29. nini_datacomp-0.1.0/src/nini_datacomp/core/blocks.py +112 -0
  30. nini_datacomp-0.1.0/src/nini_datacomp/core/compare.py +203 -0
  31. nini_datacomp-0.1.0/src/nini_datacomp/core/keys.py +138 -0
  32. nini_datacomp-0.1.0/src/nini_datacomp/core/memory.py +73 -0
  33. nini_datacomp-0.1.0/src/nini_datacomp/core/rowhash.py +194 -0
  34. nini_datacomp-0.1.0/src/nini_datacomp/errors.py +17 -0
  35. nini_datacomp-0.1.0/src/nini_datacomp/io.py +130 -0
  36. nini_datacomp-0.1.0/src/nini_datacomp/options.py +101 -0
  37. nini_datacomp-0.1.0/src/nini_datacomp/pipeline.py +824 -0
  38. nini_datacomp-0.1.0/src/nini_datacomp/py.typed +0 -0
  39. nini_datacomp-0.1.0/src/nini_datacomp/report.py +595 -0
  40. nini_datacomp-0.1.0/src/nini_datacomp/results.py +256 -0
  41. nini_datacomp-0.1.0/src/nini_datacomp/windows.py +530 -0
  42. nini_datacomp-0.1.0/tests/conftest.py +4 -0
  43. nini_datacomp-0.1.0/tests/golden/basic.json +82 -0
  44. nini_datacomp-0.1.0/tests/golden/categorical_cast_right.json +42 -0
  45. nini_datacomp-0.1.0/tests/golden/categorical_vs_string.json +35 -0
  46. nini_datacomp-0.1.0/tests/golden/composite_key.json +43 -0
  47. nini_datacomp-0.1.0/tests/golden/duplicate_keys.json +31 -0
  48. nini_datacomp-0.1.0/tests/golden/empty_left.json +31 -0
  49. nini_datacomp-0.1.0/tests/golden/floats.json +70 -0
  50. nini_datacomp-0.1.0/tests/golden/int_widths_left.json +42 -0
  51. nini_datacomp-0.1.0/tests/golden/int_widths_none.json +35 -0
  52. nini_datacomp-0.1.0/tests/golden/no_common_keys.json +31 -0
  53. nini_datacomp-0.1.0/tests/golden/null_keys.json +41 -0
  54. nini_datacomp-0.1.0/tests/golden/shuffled.json +82 -0
  55. nini_datacomp-0.1.0/tests/golden/sparse_keys.json +41 -0
  56. nini_datacomp-0.1.0/tests/golden/temporal.json +68 -0
  57. nini_datacomp-0.1.0/tests/golden/text_exact.json +61 -0
  58. nini_datacomp-0.1.0/tests/golden/text_ignore.json +48 -0
  59. nini_datacomp-0.1.0/tests/golden/text_key.json +41 -0
  60. nini_datacomp-0.1.0/tests/golden/tolerance_abs.json +61 -0
  61. nini_datacomp-0.1.0/tests/golden/tolerance_per_column.json +61 -0
  62. nini_datacomp-0.1.0/tests/golden/tolerance_rel.json +66 -0
  63. nini_datacomp-0.1.0/tests/golden/wide_300.json +364 -0
  64. nini_datacomp-0.1.0/tests/test_allocator.py +54 -0
  65. nini_datacomp-0.1.0/tests/test_api.py +232 -0
  66. nini_datacomp-0.1.0/tests/test_arrow.py +220 -0
  67. nini_datacomp-0.1.0/tests/test_benchstats.py +38 -0
  68. nini_datacomp-0.1.0/tests/test_blocks.py +59 -0
  69. nini_datacomp-0.1.0/tests/test_compare.py +194 -0
  70. nini_datacomp-0.1.0/tests/test_data.py +95 -0
  71. nini_datacomp-0.1.0/tests/test_golden.py +93 -0
  72. nini_datacomp-0.1.0/tests/test_io.py +75 -0
  73. nini_datacomp-0.1.0/tests/test_keys.py +223 -0
  74. nini_datacomp-0.1.0/tests/test_memory.py +95 -0
  75. nini_datacomp-0.1.0/tests/test_native.py +67 -0
  76. nini_datacomp-0.1.0/tests/test_pipeline.py +294 -0
  77. nini_datacomp-0.1.0/tests/test_read_experiment.py +23 -0
  78. nini_datacomp-0.1.0/tests/test_report.py +122 -0
  79. nini_datacomp-0.1.0/tests/test_rowhash.py +216 -0
  80. nini_datacomp-0.1.0/tests/test_windows.py +197 -0
  81. nini_datacomp-0.1.0/tools/bench_baseline.py +266 -0
  82. nini_datacomp-0.1.0/tools/bench_hash.py +213 -0
  83. nini_datacomp-0.1.0/tools/bench_rows.py +198 -0
  84. nini_datacomp-0.1.0/tools/benchstats.py +87 -0
  85. nini_datacomp-0.1.0/tools/cases.py +223 -0
  86. nini_datacomp-0.1.0/tools/check.py +71 -0
  87. nini_datacomp-0.1.0/tools/diag_memory.py +313 -0
  88. nini_datacomp-0.1.0/tools/diag_profile.py +99 -0
  89. nini_datacomp-0.1.0/tools/diag_read.py +118 -0
  90. nini_datacomp-0.1.0/tools/gen_bindings.py +143 -0
  91. nini_datacomp-0.1.0/tools/make_data.py +235 -0
  92. nini_datacomp-0.1.0/tools/make_golden.py +116 -0
  93. nini_datacomp-0.1.0/tools/read_experiment.py +222 -0
  94. nini_datacomp-0.1.0/uv.lock +1226 -0
@@ -0,0 +1,4 @@
1
+ # arquivos com bytes fixos em qualquer sistema (o Git no Windows trocaria LF por CRLF)
2
+ *.json text eol=lf
3
+ *.parquet binary
4
+ *.zip binary
@@ -0,0 +1,23 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+ .pytest_cache/
9
+
10
+ # Virtual environments
11
+ .venv
12
+
13
+ # Data folder
14
+ data/
15
+
16
+ # Benchmarks
17
+ benchmarks/results/
18
+
19
+ # Demo
20
+ reports/
21
+ # Dados gerados (tools/make_data.py) e resultados de benchmark
22
+ benchmarks/
23
+ amostras.md
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,281 @@
1
+ # Changelog
2
+
3
+ Formato: [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões: [SemVer](https://semver.org/lang/pt-BR/).
4
+
5
+ ## [Não lançado]
6
+
7
+ ## [0.1.0] — 2026-11-03
8
+
9
+ Primeira versão: a reescrita do datacompolars 0.4.4 com a comparação em kernels C++.
10
+
11
+ - `dc.compare(left, right, on, ...)` com `CompareOptions` e `ComparisonResult`: o mesmo resultado do datacompolars
12
+ (contagens, divergências por coluna e amostras, conferidos contra ele em 21 casos de borda), com os nomes da
13
+ tabela de migração do README.
14
+ - Comparação por janelas pela chave, dentro de um `memory_limit`: todos os casos da grade (1M e 10M linhas x 10, 50
15
+ e 300 colunas) ficam abaixo de 1 GB, onde o datacompolars chegava a quase 2 GB.
16
+ - Igualdade exata pelo hash de linha (`verify_hash=True` confere célula a célula); tolerâncias, `ignore_case` e
17
+ `ignore_whitespace` coluna a coluna, com a aritmética do Polars.
18
+ - Relatório em texto, Markdown, HTML e JSON (`result.report`).
19
+ - Desempenho (caminho exato, `memory_limit="1GB"`): de 0,66x a 1,05x o tempo do datacompolars, e 10M x 50 em 1,20x
20
+ (inconclusivo: o IC de 1,05 a 1,35 passa de 1,10, mas não fica inteiro acima). Nenhum caso mais lento.
21
+ - Wheels para Linux, Windows e macOS (x86-64 e ARM64), Python 3.10 a 3.14, sem pyarrow.
22
+
23
+ O histórico abaixo, fase a fase, é o do desenvolvimento.
24
+
25
+ ### Fase 0a — fundação
26
+ - Biblioteca C++ (`datacomp.dll` / `libdatacomp.so` / `.dylib`) com ABI em C de prefixo `dc_`, versão de ABI conferida
27
+ no carregamento (`DC_ABI_VERSION`) e assinaturas do ctypes geradas dos `.cpp` (`tools/gen_bindings.py`).
28
+ - Do nini-datagen 0.3.0: build com scikit-build-core e CMake (clang-cl nos wheels do Windows, x86-64-v2, sem FMA,
29
+ `/MT`), pool de threads (`core/blocks.py`), pool de memória (`cpp/memory.cpp`, `core/memory.py`), interface C do
30
+ Arrow sem pyarrow (`core/arrow_c.py`) e os workflows de CI e de wheels.
31
+ - `core/arrow.py`: `import_column` lê qualquer coluna dos tipos da 0.1 (inteiros, reais, booleano, texto, Date,
32
+ Datetime, Duration, Time, Decimal) como buffers do Arrow, sem cópia; outro tipo levanta `UnsupportedTypeError`.
33
+ - API pública por enquanto: `release_memory()`, `pool_limit()`, `pool_stats()`, `__version__`.
34
+
35
+ ### Fase 0b — dados, gabaritos e linha de base
36
+ - `tools/make_data.py`: pares de tabelas Parquet gerados com o nini-datagen (semente fixa; inteiros, reais, texto,
37
+ booleano, data e data e hora em ciclo, ~2% de nulos), com o lado direito mudado de forma controlada e o resultado
38
+ esperado gravado pela própria construção (`expected.json`).
39
+ - `tools/cases.py` + `tools/make_golden.py`: 21 casos de borda (chave composta, de texto, nula, esparsa, fora de
40
+ ordem; duplicatas; lado vazio; -0.0, NaN, infinitos; texto com acentos, NBSP e emoji; Categorical; larguras de
41
+ inteiro; tolerâncias na borda; 300 colunas; tipos temporais e Decimal) com gabaritos do datacompolars 0.4.4 pelo
42
+ caminho columnwise em `tests/golden/`. Alguns conferidos à mão nos testes.
43
+ - `tools/bench_baseline.py` + `tools/benchstats.py`: linha de base do datacompolars 0.4.4 na grade do plano (100 a 10M
44
+ linhas x 10 a 300 colunas, caminhos hash e columnwise), cada execução num processo novo, média e IC de 95%,
45
+ retomável; resultado em `docs/benchmarks/baseline.{md,json}`.
46
+
47
+ ### Fase 0c — experimento de leitura
48
+ - `tools/read_experiment.py`: lê as mesmas janelas da comparação (faixas de ids, os dois lados) pelo Polars, pelo
49
+ leitor C++ do Apache Arrow, pelo Polars sem compressão e só os bytes do disco, para medir o teto do ganho de um
50
+ leitor de Parquet próprio antes de escrevê-lo. Grupo `bench` (pyarrow) só para isso; o pacote não usa o pyarrow.
51
+ - Gabaritos: o tipo divergente guarda só o nome do tipo (o texto do Categorical muda entre versões do Polars).
52
+ - Decimal: o Polars 1.30 não o exporta pela interface C do Arrow (nem pela representação física); exige um Polars
53
+ mais novo.
54
+
55
+ ### Fase 1a — alinhamento das chaves (ABI 2)
56
+ - Kernel `dc_align_keys` (`cpp/keys.cpp`) e `core/keys.py` (`align_keys`): pares de linhas com a mesma chave e as
57
+ contagens do datacompolars (em comum, só esquerda, só direita, excedentes por chave repetida). Chave de qualquer tipo
58
+ da 0.1 e composta; nulo casa com nulo, `-0.0 == 0.0`, NaN casa com NaN. Merge linear quando os dois lados vêm
59
+ ordenados (caminho direto para chave inteira sem nulos), hash join quando não; com chave repetida, só as contagens.
60
+ - Medido (Linux, 1 thread, chave int64): merge ~7 ns/linha; hash ~27 ns/linha numa janela de 100 mil linhas e ~59 em
61
+ 1 milhão. Particionar o hash e paralelizar ficam para a fase 5.
62
+ - `import_column` recusa colunas codificadas por dicionário (Categorical, Enum): os buffers seriam códigos, diferentes
63
+ em cada tabela. A comparação converte para String antes.
64
+
65
+ ### Fase 1b — janelas por faixa de qualquer chave (ABI 3)
66
+ - `windows.py` (`plan_windows`): janelas = faixas da tupla inteira da chave (ordem lexicográfica), de qualquer tipo
67
+ ordenável — inteiro, texto, data, chave composta; acaba a exigência do datacompolars de 1ª coluna inteira. Chaves com
68
+ nulo vão para a primeira janela. Fronteiras = a chave a cada N linhas do lado maior, por uma leitura só das colunas
69
+ da chave.
70
+ - Tamanho da janela pelo `memory_limit` (`"auto"` = 1/4 da memória livre, até 2 GiB; ou `"2GB"`, `"1.5GiB"`, bytes):
71
+ N = orçamento x 0,8 / (2 janelas x (bytes por linha dos dois lados + trabalho dos kernels)), com os bytes por linha
72
+ medidos pelo Polars numa amostra (memória depois de lida, não o tamanho no disco). `window_rows` fixa N à mão.
73
+ - Com o lado maior gravado em ordem, as fronteiras vão para o começo do row group mais próximo: nenhum row group é
74
+ decodificado duas vezes. As linhas de cada row group vêm do rodapé do Parquet, lido por `cpp/parquet_meta.cpp`
75
+ (`dc_parquet_row_groups`, Thrift compacto, sem pyarrow).
76
+ - `io.py`: fontes Parquet (arquivo, pasta com subpastas, glob), `pl.DataFrame` e `pl.LazyFrame`; CSV e SAS na fase 6.
77
+
78
+ ### Fase 1c — caminho completo: contagem de linhas por janelas
79
+ - `pipeline.py`: `count_rows(left, right, on, memory_limit=..., window_rows=..., columns=...)` abre as fontes, confere
80
+ a chave (Categorical/Enum viram texto; tipos diferentes nos dois lados = erro claro), planeja as janelas, lê cada uma
81
+ pelo Polars e alinha as chaves no C++, somando as contagens. `WindowReader` lê a janela k+1 numa thread enquanto a k
82
+ é processada (no máximo duas em memória), e é a base das fases 2 e 3.
83
+ - A primeira janela só leva a cláusula "ou chave nula" quando algum lado tem chave nula: o "ou" impedia o Polars de
84
+ pular row groups pelas estatísticas, e a primeira janela leria a tabela inteira.
85
+ - `tools/bench_rows.py`: memória e tempo de 1M a 10M linhas (chave inteira e chave de texto), cada execução num
86
+ processo novo; resultado em `docs/benchmarks/fase1.{md,json}`.
87
+ - Medido no primeiro benchmark (`memory_limit = 1GB`): contagens certas em tudo, mas pico de 1,6–2,1 GB em 10M linhas,
88
+ crescendo com as linhas. Corrigido no planejamento: ele não carrega mais todas as chaves (nem cópias ordenadas) —
89
+ só conta as linhas numa passada, lê a chave do começo de cada row group (Parquet em ordem) ou uma amostra de até 200
90
+ mil chaves; a janela passa a contar 4 cópias vivas no pico (medido), não 2; e a base é o lado em ordem no disco
91
+ (antes era sempre o maior, que no make_data tem os arquivos extra-* fora de ordem).
92
+ - Medido no segundo (`memory_limit = 1GB`): pico de 1,3–1,6 GB em 10M linhas com a maior janela em ~200 MB. Para achar
93
+ de onde vem o resto: `engine` no `count_rows`/`WindowReader` (o engine do `collect` do Polars em cada janela) e
94
+ `tools/diag_memory.py`, que anota a memória etapa a etapa e janela a janela e roda variações que isolam uma causa
95
+ por vez (sem leitura em paralelo, menos threads, engine streaming, só a chave, janela menor, alocador devolvendo a
96
+ memória na hora, sem os kernels, sem o pool).
97
+ - Diagnóstico: o resto do pico era memória que o alocador do Polars (mimalloc, no Windows) guarda depois de liberar e
98
+ vai acumulando janela após janela; janelas menores, sem leitura em paralelo, sem os kernels ou sem o pool quase não
99
+ mudavam o pico. Com `MIMALLOC_PURGE_DELAY=0`: 10M x 10 de 1159 para 605 MB, 10M x 50 de 1094 para 693 MB (e 5,6 →
100
+ 4,2 s nesse caso).
101
+ - `core/allocator.py`: o import do `nini_datacomp` define `MIMALLOC_PURGE_DELAY=0` quando o Polars ainda não foi
102
+ importado (sem sobrescrever um valor existente); quando já foi, avisa com `AllocatorWarning` (no Windows).
103
+ `NINI_DATACOMP_ALLOCATOR=0` desliga; `nini_datacomp.ALLOCATOR_STATUS` diz o que aconteceu. Os benchmarks medem com o
104
+ alocador configurado.
105
+ - `count_rows` solta as tabelas de cada janela antes de pedir a próxima: a variável do laço segurava a janela k
106
+ enquanto o leitor entregava a k+1 e já lia a k+2 (três janelas vivas; ~200 MB a mais no pico em 10M linhas, a
107
+ diferença entre o bench_rows e o diag_memory).
108
+
109
+ ### Fase 2 — hash de linha (ABI 4)
110
+ - Kernels `dc_row_hash` e `dc_diff_pairs` (`cpp/rowhash.cpp`) e `core/rowhash.py` (`row_hash`, `diff_pairs`): um hash de
111
+ 64 bits por linha das colunas comparadas, por blocos de 65.536 linhas em paralelo, e os pares (da `align_keys`) com
112
+ hash diferente. Hash próprio, igual em toda plataforma e versão do Polars (valores fixados nos testes), com a
113
+ igualdade do `eq_missing`: nulo = nulo, `-0.0 == 0.0`, NaN = NaN, texto inline ou em buffer, com ou sem bitmap de
114
+ validade. Construção: `h = fmix64((h ^ valor) + sal da coluna)` — o fmix64 (do murmur3) é uma bijeção, então duas
115
+ linhas que diferem numa coluna só nunca colidem; o plano falava em wyhash, que precisaria de multiplicação de 128
116
+ bits (não portátil no clang-cl com `-Wpedantic`), sem ganho de qualidade aqui.
117
+ - `verify=True` (`verify_hash` no `compare_rows`) reconfere célula a célula os pares de hash igual e conta as colisões.
118
+ - Medido (Linux, 1 thread): ~3 ns por célula, inteiro ou real.
119
+ - `cpp/columns.hpp`: a leitura das colunas (descritores, valores, igualdade) sai do `keys.cpp` para os dois kernels.
120
+ - `pipeline.compare_rows(left, right, on, casting=..., columns=..., verify_hash=...)`: as contagens do `count_rows` e
121
+ as linhas divergentes (`mismatched`, `equal`). Colunas comparadas como no datacompolars: nos dois lados, fora da
122
+ chave, com o mesmo tipo depois do `casting` (`"left"`/`"right"` convertem um lado para os tipos do outro, também na
123
+ chave); as de tipo diferente vão para `type_mismatches`; Categorical/Enum comparados como texto; colunas aninhadas
124
+ (List, Struct) levantam `UnsupportedTypeError` (fase 6). Só as colunas comparadas são lidas.
125
+ - Testes: os gabaritos de igualdade exata (linhas divergentes, colunas comparadas, tipos divergentes), com uma janela
126
+ e com janelas de 2 linhas, e os dados do make_data.
127
+ - `tools/bench_hash.py`: tempo e memória de 1M a 10M linhas x 10 a 300 colunas contra o caminho hash do
128
+ datacompolars, com a razão e o IC de 95%; resultado em `docs/benchmarks/fase2.{md,json}`.
129
+ - Primeiro benchmark (`memory_limit = 1GB`): contagens certas em tudo, mas mais lento que o datacompolars (10M x 300:
130
+ 80 s contra 50 s) e acima do limite (1,5 GB). Duas causas corrigidas:
131
+ - o hash juntava cada coluna da janela num pedaço só de memória (`rechunk`, uma cópia da janela inteira por lado)
132
+ quando o Polars a entregava em vários (um por row group): agora `row_hash_parts` lê pedaço a pedaço, sem cópia
133
+ (o `verify_hash` ainda junta, porque lê fora de ordem). O `import_column` nem tenta exportar uma coluna em vários
134
+ pedaços (a exportação falharia e custava tempo).
135
+ - com 300 colunas o orçamento dava janelas de ~52 mil linhas, menores que os row groups de 100 mil: cada row group
136
+ era decodificado duas vezes. A janela agora nunca é menor que o maior row group (com uma nota no plano).
137
+ - Segundo benchmark: contagens certas; 10M x 300 em 43 s (0,86x o datacompolars, `ok`), 10M x 10 e 1M x 300 `ok`;
138
+ 10M x 50 (1,13x) e 1M x 50 (1,58x) ainda mais lentos. O tempo estava no hash (30 s dos 43 em 10M x 300), e não no
139
+ kernel: ler as colunas uma a uma pelo Python (`import_column`) custa ~0,3 ms por coluna, e uma janela de 300 colunas
140
+ e 100 mil linhas tinha só 2 blocos de 65.536 linhas para 16 threads. Corrigido:
141
+ - `import_frame` (`core/arrow.py`) exporta o pedaço do DataFrame inteiro de uma vez (`__arrow_c_stream__` do
142
+ DataFrame, um struct do Arrow) e `dc_describe_batch` (`cpp/batch.cpp`) preenche os descritores de todas as colunas
143
+ numa chamada só. Sem lote único (Categorical, Polars antigo, vários pedaços), volta à leitura coluna a coluna.
144
+ - o hash divide as linhas em tarefas de 4.096 a 65.536 linhas, ao menos ~4 por thread (o resultado não muda).
145
+ - Terceiro benchmark: o hash caiu para 6 s em 10M x 300 (de 30) e o pico para ~1,2 GB (de 1,6), mas o tempo total
146
+ ficou em 44,5 s: agora é a leitura que não fica escondida (`read_wait` 30 s). Ela crescia mais que as linhas
147
+ (10M x 300 leva 10x as linhas e 10x as janelas de 1M x 300, mas ~17x a leitura) porque cada janela lia a fonte
148
+ inteira: o Polars abria o rodapé dos 100 arquivos de cada lado a cada uma das 100 janelas (com 300 colunas, um
149
+ rodapé grande). Agora o planejamento anota a faixa da 1ª coluna da chave de cada arquivo (uma passada pela chave) e
150
+ cada janela lê só os arquivos que encostam nela (`Source.scan`, com as mesmas conversões de tipo da fonte).
151
+ - Diagnóstico da leitura (`tools/diag_read.py`): o Polars não tem custo fixo alto por leitura; o tempo é proporcional
152
+ às células (~3,5 ns cada, também com o disco frio), e o corte de arquivos por janela ajuda pouco (fica). O que pesa
153
+ é o tamanho: os row groups de 100 mil linhas dos dados de benchmark eram arbitrários.
154
+ - Dados de benchmark com o row group padrão do Polars (`Spec.row_group_size = None`; a pasta ganha `_rg<n>` quando o
155
+ row group é fixo) e arquivos de ~100 milhões de células (antes 30 milhões: um row group por arquivo nas tabelas
156
+ largas). O `read_experiment` (fase 0c) continua com os row groups de 100 mil.
157
+ - Grupos de colunas: quando um row group dos dois lados não cabe no `memory_limit`, cada janela (um row group) é lida
158
+ em grupos de colunas seguidas, cada um com a chave, e o hash de linha continua de um grupo para o outro
159
+ (`dc_row_hash(..., first, ...)`, ABI 5: mesmos bits que todas as colunas juntas). A memória passa a depender do
160
+ grupo, não do row group. `Plan.column_groups`/`split_columns`, `WindowReader(groups=...)`/`pieces()`,
161
+ `RowCounts.column_groups`. O `verify_hash` confere célula a célula grupo a grupo e junta os pares diferentes.
162
+ - `tools/check.py`: formatação, lint, tipos e testes em sequência, parando no primeiro que falhar; `--fix` e
163
+ `--then <ferramenta>` (benchmarks no fim).
164
+ - Benchmark global (dados novos, linha de base refeita): divergentes certos em tudo; todos os picos abaixo de 1 GB
165
+ (10M x 300: 588 MB em 3 grupos de colunas, 47,1 s, 0,94x o datacompolars); mas 1M x 50 (1,41x) e 10M x 50
166
+ (1,56x) mais lentos, com a leitura dominando e só ~2/3 do memory_limit em uso. Ajustes:
167
+ - `IN_FLIGHT` de 4 para 3 (o pico medido, já sem o bug das três janelas e com o alocador configurado, é de 1,9 a
168
+ 2,9x a maior janela): janelas ~1/3 maiores, menos leituras.
169
+ - os dois lados de cada janela lidos ao mesmo tempo (`WindowReader(parallel_sides=True)`): uma leitura do Polars
170
+ de poucos row groups não ocupa todas as threads.
171
+ - Resultado: 10M x 300 em 36,9 s (0,74x o datacompolars) e 917 MB; 10M x 50 de 6,7 para 5,4 s (1,27x); 1M x 50 quase
172
+ igual (1,37x). Mas a contagem com chave de texto em 10M x 10 passou do limite (1.086 MB): `IN_FLIGHT` = 3,5. Novas
173
+ etapas nos tempos: `setup` (abrir as fontes e alinhar os schemas) e `other` (o que nenhuma etapa mediu; em 10M x 50
174
+ eram ~1,6 s dos 5,4).
175
+ - Com `IN_FLIGHT` = 3,5: 10M x 300 em 36,5 s (0,73x), 1M x 300 0,88x, 10M x 10 1,01x, 10M x 50 1,01x (inconclusivo),
176
+ 1M x 50 1,31x (mais lento). O `other` era grande (10M x 300: 8,7 s; 10M x 50: 2,1 s): soltar a memória de cada
177
+ janela na thread principal, com o alocador devolvendo as páginas na hora. Agora as janelas são soltas numa thread
178
+ à parte (`_dispose`).
179
+ - Os relatórios dão o pico em MiB (`MB` = 1.048.576 bytes) e o `memory_limit` "1GB" é 10^9 bytes = 954 MiB: alguns
180
+ picos passavam do limite por até ~3% (10M x 50: 959 MiB; contagem com chave de texto em 10M x 10: até 985 MiB).
181
+ O `bench_hash` ganhou a coluna `cabe` (o maior pico contra o limite).
182
+ - Soltar as janelas noutra thread tirou o `other` (10M x 300: 8,7 → 2,6 s), mas o tempo total não mudou: o custo foi
183
+ para a leitura em paralelo (o gargalo agora é o processador, lendo). Fica, porque não atrapalha. Dois picos ainda
184
+ passavam do limite (10M x 50: 983 MiB; chave de texto em 10M x 10: 992 MiB; o limite de 1GB é 954 MiB):
185
+ `SAFETY` de 0,8 para 0,7 (janelas ~12% menores).
186
+ - Com `SAFETY` = 0,7: todos os picos dentro do limite (o maior: 10M x 50, 904 MiB) e os tempos iguais. Fase 2
187
+ fechada: 10M x 300 em 36,5 s (0,73x o datacompolars, 686 MiB); ficam para a fase 5 o 1M x 50 (1,32x) e o
188
+ 10M x 50 (1,03x, inconclusivo).
189
+
190
+ ### Fase 3 — coluna a coluna (ABI 6)
191
+ - Kernel `dc_compare_column` (`cpp/compare.cpp`) e `core/compare.py` (`compare_column`, `tolerance_for`): uma coluna,
192
+ par a par, exata (o `eq_missing` do Polars, NaN = NaN) ou com tolerância nas numéricas (`|l - r| <= abs_tol +
193
+ rel_tol * |r|`, com a aritmética do Polars: inteiros no próprio tipo, com volta; Float32 em float; Decimal pelo
194
+ inteiro de 128 bits). Nulo dos dois lados = igual. Conferido nos testes contra a própria expressão do datacompolars
195
+ rodando no Polars instalado, em todos os tipos inteiros, nos reais e no Decimal.
196
+ - `compare_rows(..., abs_tol=..., rel_tol=..., ignore_case=..., ignore_spaces=...)` e `RowCounts.column_mismatches`
197
+ (divergências por coluna). Em cada grupo de colunas de uma janela: as exatas pelo hash de linha e, só nos pares
198
+ divergentes (poucos: um `gather` do Polars), a contagem coluna a coluna; as com tolerância, uma a uma, em todos os
199
+ pares. Cada grupo tem o seu hash (um par diverge se diverge em algum grupo), então a contagem por coluna sai
200
+ enquanto o grupo está em memória.
201
+ - Texto com `ignore_case`/`ignore_spaces`: normalizado na leitura com as mesmas expressões do datacompolars
202
+ (`str.to_lowercase`, `str.replace_all(r"\s+", "")`), e depois comparado exato (pelo hash). O plano previa caixa e
203
+ espaços no C++ (ASCII e Latin-1 rápidos, UTF-8 completo); com as expressões do Polars o resultado bate com o do
204
+ datacompolars em qualquer texto Unicode, e o texto normalizado ainda vai pelo hash. O C++ fica para a fase 5, se o
205
+ perfil mandar.
206
+ - Testes: todos os 21 gabaritos agora (tolerâncias e opções de texto inclusive), também por coluna e com
207
+ `verify_hash`; grupos de colunas com tolerância.
208
+ - `tools/bench_hash.py --mode columnwise`: `abs_tol=1e-12`, contra o columnwise da linha de base, conferindo também
209
+ as contagens por coluna; resultado em `docs/benchmarks/fase3.{md,json}`.
210
+ - Os testes contra o Polars confirmaram a aritmética dos inteiros (com volta, também os sem sinal) e acharam uma
211
+ diferença nos reais: o Polars compara reais em ordem total (NaN maior que tudo, NaN = NaN), não pelo IEEE. Na
212
+ tolerância isso importa: com r infinito, `rel_tol * |r|` pode dar `0 * inf = NaN`, e para o Polars `x <= NaN` é
213
+ verdade (o datacompolars dá esses pares como iguais). O kernel agora compara como o Polars (`polars_le`).
214
+ - `column_mismatches` lista todas as colunas comparadas (as sem divergência com 0); o datacompolars lista só as
215
+ com divergência.
216
+ - CI (min-deps, Polars 1.30): o Decimal exportado como Series sai com o formato `_pli128` (o mesmo inteiro de 128
217
+ bits); o hash passava (o DataFrame inteiro sai como `d:`), mas o `verify_hash` e a coluna a coluna recusavam a
218
+ coluna. `_pli128` aceito como Decimal no `import_column` e no `dc_describe_batch`.
219
+ - Benchmark coluna a coluna (`abs_tol = 1e-12`): contagens certas (também por coluna) em tudo; 10M x 300 em 37,2 s
220
+ (0,57x o columnwise do datacompolars, 805 MiB); 1M x 50, 1M x 300, 10M x 10 e 10M x 50 `ok`; 1M x 10
221
+ inconclusivo (1,17x). Um pico de 10M x 50 passou o limite em 1% (964 MiB): `SAFETY` = 0,65.
222
+
223
+ ### Fase 4a — API pública
224
+ - `dc.compare(left, right, on, options=None, **kwargs)`: a função pública. As opções em `CompareOptions` (pydantic,
225
+ `extra="forbid"`) ou como argumentos nomeados; com os dois, os nomeados ganham.
226
+ - `CompareOptions`: `names`, `case_sensitive_columns`, `cast`, `abs_tol`/`rel_tol` (número ou por coluna),
227
+ `ignore_whitespace`, `ignore_case`, `details`, `samples`, `memory_limit`, `window_rows`, `threads`, `verify_hash`.
228
+ - `ComparisonResult` (dataclasses imutáveis): `rows`, `columns` como mapeamento por nome (`result.columns["v"]`, com
229
+ `mismatches`, `rate`, `dtype`, `tolerance` e as amostras em `pl.DataFrame`), `columns.mismatched()` na ordem do
230
+ datacompolars, `errors`, `warnings`, `execution`, `is_match`, `aborted`, `to_dict()`/`to_json()`.
231
+ - Amostras: as primeiras `samples` linhas divergentes de cada coluna, na ordem da chave, com cabeçalhos
232
+ `[*chave, "<coluna> (<esquerda>)", "<coluna> (<direita>)"]`, como no datacompolars. Com `ignore_case`/
233
+ `ignore_whitespace`, o texto normalizado é lido ao lado do original e as amostras mostram o valor de verdade.
234
+ - Erros fatais no resultado, não em exceção (como no datacompolars): chave que falta, chave de tipos diferentes sem
235
+ `cast`, chave repetida, nomes que colidem em minúsculas. Avisos: nenhuma coluna além da chave; tolerância por coluna
236
+ com nome que não é comparado.
237
+ - Chave repetida sempre aborta: não há `check_unique_keys=False` (o plano previa). A checagem sai de graça do
238
+ alinhamento das chaves, e desligá-la só serviria para contagens infladas.
239
+ - Testes: os 21 gabaritos pela API, inclusive as amostras valor a valor, com uma janela e com janelas de 2 linhas.
240
+
241
+ ### Fase 4b — relatório e publicação
242
+ - `result.report(format=None, path=None, *, width=100, style="auto", max_columns=20, max_sample_columns=10,
243
+ print_output=False)`: o relatório do datacompolars nos mesmos quatro formatos (texto, Markdown, HTML, JSON),
244
+ portado para o resultado novo. Sem `format`, sai da extensão de `path`; com `path`, também salva. Diferente do
245
+ datacompolars, não imprime sozinho (`print_output=True` imprime).
246
+ - A seção "Execution" diz como cada coluna foi comparada (hash de linha ou coluna a coluna com tolerância), as
247
+ janelas, os grupos de colunas, o `memory_limit` e a maior janela, os tempos por etapa e as versões.
248
+ - README: uso e a tabela de migração do datacompolars.
249
+ - Versão `0.1.0rc1` para o TestPyPI; o teste do TestPyPI nas máquinas limpas agora roda uma comparação de verdade e
250
+ o relatório nos quatro formatos.
251
+
252
+ ### Fase 5a — desempenho (antes da 0.1.0)
253
+ - Planejamento das janelas: as linhas de um Parquet saem do rodapé (sem ler dados) e as consultas que restam (a
254
+ amostra de cada lado para os bytes por linha, as chaves nulas, a chave no começo de cada row group) rodam juntas,
255
+ em paralelo, em vez de uma depois da outra (em 1M x 50, o planejamento levava ~0,14 s de 0,59 s).
256
+ - `tools/diag_profile.py`: o cProfile de uma comparação pela API, com as etapas de cada execução.
257
+ - O planejamento faz uma rodada só de consultas ao Polars: a chave no começo de cada row group e a faixa de chaves
258
+ de cada arquivo não dependem do tamanho das janelas e vão junto com a amostra, as chaves nulas e as linhas.
259
+ - As colunas da chave de cada janela numa exportação só do Polars por lado (`align_frames`), em vez de uma por
260
+ coluna.
261
+ - Amostras montadas a partir de listas do Python (uma conversão por lado), em vez de indexar uma Series do Polars por
262
+ valor (cada indexação passa por uma consulta).
263
+ - `tools/check.py --then "bench_hash --cases 1000000x50" diag_profile`: cada ferramenta entre aspas leva os próprios
264
+ argumentos.
265
+ - Resultado (fase 2, caminho exato, `memory_limit="1GB"`): 1M x 50 de 1,39x para 1,05x (ok) e o pico de 801 para
266
+ 724 MiB; 1M x 10 0,81x, 1M x 300 0,81x, 10M x 10 0,96x, 10M x 300 0,66x (32,9 s), todos ok; 10M x 50 1,20x,
267
+ inconclusivo. Contagens certas em todos, também no caminho coluna a coluna (fase 3).
268
+ - Bytes por linha: os tipos de largura fixa (inteiros, reais, datas, Decimal, booleano) pelo tipo; só as colunas de
269
+ largura variável (texto, Categorical) vão para a amostra. A amostra lia o primeiro row group inteiro de todas as
270
+ colunas: em 1M x 50, ~50 ms dos ~80 ms do planejamento.
271
+ - `execution.window_bytes` pelos bytes por linha do plano, sem o `estimated_size` do Polars (alguns ms por janela,
272
+ segurando o GIL enquanto a comparação roda).
273
+ - Contagem por coluna nos pares divergentes: `dc_compare_pairs` (ABI 8) compara todas as colunas numa chamada,
274
+ lendo direto dos pedaços de memória da janela que o hash de linha já leu (sem reunir as linhas no Polars, cujas
275
+ threads estão ocupadas lendo a janela seguinte); antes, eram uma `dc_compare_column` e duas exportações do Polars
276
+ por coluna (em 1M x 50, ~0,12 s por comparação). `dc_compare_rows` (`compare_frames`) faz o mesmo com as linhas
277
+ já reunidas.
278
+ - Amostras: os pares divergentes vão para a ordem da chave uma vez por janela, e as amostras de cada coluna saem das
279
+ linhas já reunidas para a contagem (as primeiras em que ela diverge); antes, um `select`, um `sort` e um `head` do
280
+ Polars por coluna (em 1M x 50 com `samples=5`, ~0,13 s).
281
+
@@ -0,0 +1,68 @@
1
+ cmake_minimum_required(VERSION 3.20)
2
+ project(nini_datacomp LANGUAGES CXX)
3
+
4
+ # Biblioteca compartilhada com ABI em C (cpp/datacomp.h), carregada pelo Python via ctypes.
5
+ # Gera libdatacomp.so (Linux), libdatacomp.dylib (macOS) ou datacomp.dll (Windows) dentro do pacote nini_datacomp.
6
+
7
+ set(CMAKE_CXX_STANDARD 17)
8
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
9
+ set(CMAKE_CXX_EXTENSIONS OFF)
10
+ set(CMAKE_CXX_VISIBILITY_PRESET hidden) # só as funções DC_EXPORT ficam visíveis
11
+ set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
12
+ if(NOT CMAKE_BUILD_TYPE)
13
+ set(CMAKE_BUILD_TYPE Release)
14
+ endif()
15
+
16
+ # MSVC: runtime do C++ embutido na DLL (/MT). Sem isto, datacomp.dll depende do msvcp140.dll, que o Python não traz e
17
+ # que só existe em máquinas com o Visual C++ Redistributable instalado.
18
+ set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
19
+
20
+ option(DC_NATIVE "Compilar para a CPU desta máquina (-march=native); nunca para wheels publicados" OFF)
21
+
22
+ file(GLOB DC_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/cpp/*.cpp")
23
+ add_library(datacomp SHARED ${DC_SOURCES})
24
+ target_include_directories(datacomp PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/cpp")
25
+
26
+ set(DC_X86 OFF)
27
+ if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64|x64")
28
+ set(DC_X86 ON)
29
+ endif()
30
+
31
+ # Mesmo resultado em qualquer sistema: a tolerância |l - r| <= abs_tol + rel_tol * |r| não pode depender da CPU nem do
32
+ # compilador, inclusive exatamente na borda. Sem fusão de multiplicação e soma (-ffp-contract=off: FMA arredonda
33
+ # diferente, e o ARM e as CPUs com FMA fundiriam a*b + c por conta própria) e sem matemática "rápida".
34
+ # -fno-math-errno e -fno-trapping-math não mudam nenhum resultado (só dizem que ninguém lê errno nem captura exceções
35
+ # de ponto flutuante) e deixam os laços vetorizar.
36
+ if(MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
37
+ # clang-cl (DC_CLANGCL=1 no Windows): opções do clang passadas com /clang:
38
+ target_compile_options(datacomp PRIVATE /utf-8 /W4 /clang:-O3 /clang:-ffp-contract=off /clang:-fno-math-errno /clang:-fno-trapping-math)
39
+ if(DC_NATIVE)
40
+ target_compile_options(datacomp PRIVATE /clang:-march=native)
41
+ elseif(DC_X86)
42
+ target_compile_options(datacomp PRIVATE /clang:-march=x86-64-v2)
43
+ endif()
44
+ target_compile_definitions(datacomp PRIVATE _CRT_SECURE_NO_WARNINGS)
45
+ elseif(MSVC)
46
+ # /utf-8: os comentários têm acentos; /O2 já vem do Release.
47
+ # /fp:precise (padrão desde o VS 2022) não funde multiplicação e soma.
48
+ target_compile_options(datacomp PRIVATE /utf-8 /W4 /permissive- /Zc:__cplusplus /fp:precise)
49
+ target_compile_definitions(datacomp PRIVATE _CRT_SECURE_NO_WARNINGS)
50
+ else()
51
+ target_compile_options(datacomp PRIVATE -O3 -Wall -Wextra -Wpedantic -ffp-contract=off -fno-math-errno -fno-trapping-math)
52
+ if(DC_NATIVE)
53
+ target_compile_options(datacomp PRIVATE -march=native)
54
+ elseif(DC_X86)
55
+ # base dos wheels: x86-64-v2 (SSE4.2/POPCNT). AVX2 entra depois (fase 5) como segunda cópia escolhida na hora,
56
+ # só nos kernels em que o perfil mandar; FMA fica de fora de propósito (veja acima).
57
+ target_compile_options(datacomp PRIVATE -march=x86-64-v2)
58
+ endif()
59
+ endif()
60
+
61
+ if(WIN32)
62
+ set_target_properties(datacomp PROPERTIES PREFIX "") # datacomp.dll
63
+ endif()
64
+
65
+ # scikit-build-core coloca o conteúdo de CMAKE_INSTALL_PREFIX na raiz do wheel
66
+ install(TARGETS datacomp
67
+ LIBRARY DESTINATION nini_datacomp
68
+ RUNTIME DESTINATION nini_datacomp)
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Nini (B. L. Z. Rosa)
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,4 @@
1
+ nini-datacomp
2
+ Copyright 2026 Nini (B. L. Z. Rosa)
3
+
4
+ Reescrita do datacompolars (Apache-2.0, mesma autora), com partes do nini-datagen (MIT, mesma autora).