feat(sales): initial Parquet migration infrastructure #3

Open
notid wants to merge 6 commits from feat/sales-parquet-migration into master
Owner
  • Add DuckDB/S3 parquet storage layer (auto-ap.storage.parquet)
  • Add sales_to_parquet migration script for historical data
  • Add cleanup_sales for post-migration Datomic cleanup
  • Add sales_orders_new.clj with DuckDB read layer for SSR views
  • Add test scaffolding for parquet storage
  • Add plan document for move-detailed-sales-to-parquet

feat(sales): redirect production and read flows to Parquet/DuckDB

  • U3: Square production (upsert) now buffers to parquet via flatten-order-to-parquet!
  • U3: EzCater core import-order now buffers to parquet instead of Datomic transact
  • U3: EzCater XLS upload-xls now buffers to parquet instead of audit-transact
  • U4: Rewrite sales_orders.clj to read from DuckDB via pq/get-sales-orders
  • U5: Rewrite sales_summaries to use parquet aggregation functions
    • get-payment-items-parquet, get-discounts-parquet, get-refund-items-parquet
    • get-tax-parquet, get-tip-parquet, get-sales-parquet
  • Add sum-* aggregation functions to storage/sales_summaries.clj
    • sum-discounts, sum-refunds-by-type, sum-taxes, sum-tips, sum-sales-by-category
- Add DuckDB/S3 parquet storage layer (auto-ap.storage.parquet) - Add sales_to_parquet migration script for historical data - Add cleanup_sales for post-migration Datomic cleanup - Add sales_orders_new.clj with DuckDB read layer for SSR views - Add test scaffolding for parquet storage - Add plan document for move-detailed-sales-to-parquet feat(sales): redirect production and read flows to Parquet/DuckDB - U3: Square production (upsert) now buffers to parquet via flatten-order-to-parquet! - U3: EzCater core import-order now buffers to parquet instead of Datomic transact - U3: EzCater XLS upload-xls now buffers to parquet instead of audit-transact - U4: Rewrite sales_orders.clj to read from DuckDB via pq/get-sales-orders - U5: Rewrite sales_summaries to use parquet aggregation functions - get-payment-items-parquet, get-discounts-parquet, get-refund-items-parquet - get-tax-parquet, get-tip-parquet, get-sales-parquet - Add sum-* aggregation functions to storage/sales_summaries.clj - sum-discounts, sum-refunds-by-type, sum-taxes, sum-tips, sum-sales-by-category
Author
Owner

Code Review: feat/sales-parquet-migration

Scope: 15 files, ~2200 lines added (base: 297464c1, HEAD: cc838adf)
Reviewers: correctness, testing, maintainability, project-standards, agent-native, learnings
Plan coverage: R1-R5 all addressed across U1-U7. Plan file at docs/plans/2026-04-24-001-refactor-detailed-sales-to-parquet-plan.md.


P0 — Critical (Must fix before merge)

1. sales_orders_new.clj namespace collision — may break ALL sales queries

src/clj/auto_ap/datomic/sales_orders_new.clj:1 · correctness, maintainability

Both sales_orders.clj and the new sales_orders_new.clj declare (ns auto-ap.datomic.sales-orders) — the same namespace. Depending on classpath load order, one silently shadows the other. The old file is already wired to use parquet (:require [auto-ap.storage.parquet :as pq], see lines 4, 56). The new file is also broken internally:

  • raw-graphql-ids (line 157) threads from nilpq/get-sales-orders but discards the result, then references unbound result at line 165
  • Missing :require [clojure.string :as str] despite using str/join at line 52 — won't compile
  • Missing clojure.set and com.brunobonacci.mulog requires
  • get-graphql (line 209) references undefined id-keys and matching-count

Suggested fix: Delete sales_orders_new.clj. The existing sales_orders.clj has been correctly updated to use the parquet read path. If you intended to replace it, rename the new namespace and fix all undefined references before landing.

2. safe-cleanup-all destructure bug — passes wrong year/month, deletes unverified data

src/clj/auto_ap/migration/cleanup_sales.clj:199 · correctness

; collect-all-months returns [[2024 3] [2024 4] ...]  (pairs of [year month])
(doseq [[_ y m] months]   ; ← drops _=year, binds y=month, m=nil
  ...
  (verify-month-in-s3? y m)   ; → (verify-month-in-s3? 3 nil) — wrong year, nil month!)

The destructure pattern [_ y m] against a 2-element [year month] vector discards the year and binds nil to month. Both S3 verification and delete calls receive garbage arguments.

Suggested fix: Change [_ y m][y m].

3. SQL injection in WHERE clause construction

src/clj/auto_ap/storage/parquet.clj:236, sales_summaries.clj (all aggregation functions) · correctness, maintainability

User-supplied filter values (:client, :vendor, :location) are concatenated directly into SQL strings:

(str env " = '" v "'")  ; no escaping — single quotes, semicolons, or keywords break the query

Suggested fix: At minimum, escape single quotes by doubling them: (str/replace v "'" "''"). Long term, use DuckDB PreparedStatement with parameter binding.

4. query-deduped generates invalid SQL

src/clj/auto_ap/storage/parquet.clj:279-284 · correctness

(str ":sql q)
     " QUALIFY ROW_NUMBER() OVER"      ; space breaks QUALIFY from expression
     "   (PARTITION BY sales_order.external_id"  ; column doesn't exist in parquet schema
     " ORDER BY _seq_no DESC) = 1"))

The generated SQL has a space between QUALIFY and ROW_NUMBER() breaking the syntax. The partition column sales_order.external_id doesn't exist — parquet records use external_id. Function always fails at runtime.

Suggested fix: Remove leading space, correct column name: " QUALIFY ROW_NUMBER() OVER (PARTITION BY external_id ORDER BY _seq_no DESC) = 1".


P1 — High (Should fix)

5. DuckDB shutdown hook is a no-op

src/clj/auto_ap/storage/parquet.clj:26-27 · correctness, maintainability

(Thread. #(fn [])) creates a thread that returns an unevaluated function and never calls disconnect!. DuckDB connections persist until JVM crash.

Fix: (Thread. #(.close ^java.sql.Connection @db))

6. Query results/statements leak — not closed in with-open

src/clj/auto_ap/storage/parquet.clj:53-73 · correctness, performance

Neither query-scalar nor query-rows close their Statement or ResultSet objects. Under sustained load this exhausts DuckDB internal cursors and file handles.

Fix: Wrap in with-open [stmt (.createStatement conn) rs (.executeQuery stmt sql)] ...

7. flush-to-parquet! clears buffer before S3 upload completes

src/clj/auto_ap/storage/parquet.clj:148 · correctness, maintainability

Buffer is cleared at line 148, then deleted files at 149-150. But (upload-parquet! ...) runs before the clear. On upload failure the catch throws — but if an unexpected exception occurs between upload success and buffer clear, in-memory records are lost (WAL has them, but they won't re-flush until restart).

Fix: Use explicit ordering: verify S3 200 response → confirm file persisted → THEN clear buffer.

8. get-payment-items-parquet key mismatch — silently returns empty payment data

src/clj/auto_ap/jobs/sales_summaries.clj:106 · correctness

(filter #(= client-code (:client_code %)) rows)  ; ← underscored key :client_code
; but parquet records are written with (:client-code ...) — hyphenated key

The filter looks for :client_code (underscore), but all write paths (ezcater/core.clj, square/core3.clj, sales_to_parquet.clj) store as :client-code. Payment aggregation silently returns zero across all clients.

Fix: Change :client_code:client-code.

9. date-seq produces forward sequence when start > end

src/clj/auto_ap/storage/parquet.clj:203-211 · correctness

(Math/abs ...) on the epoch diff means reversing arguments still produces a valid-looking sequence going forward from start by |diff| days. Queries against non-existent S3 keys silently return empty results.

Fix: Assert start <= end or reverse iteration direction based on comparison.

10. Entity-type array duplicated in 5+ locations

parquet.clj (×2), sales_to_parquet.clj (×2), cleanup_sales.clj (×1) · maintainability, testing

The ["sales-order" "charge" "line-item" "sales-refund"] vector is hardcoded in five places with no single source of truth. Adding or renaming an entity type risks silent data gaps.

Fix: Define ENTITY-TYPES once in auto-ap.storage.parquet and require it elsewhere.


P2 — Moderate (Fix if straightforward)

  1. object-exists? downloads full S3 objects — Use s3/head-object instead of s3/get-object. Downloads entire parquet files just to check existence (~3000+ GET requests per cleanup run).

    • src/clj/auto_ap/migration/cleanup_sales.clj:113-121
  2. Perf test runs at namespace load(run-perf-tests) at line 111 triggers 100k row generation and S3 upload on every lein test. Wrap in deftest or (comment ...).

    • test/clj/auto_ap/storage/perf_test.clj:111
  3. DRY-RUN? mutable var, not thread-safe — Global boolean toggled with alter-var-root. Concurrent invocations race.

    • src/clj/auto_ap/migration/cleanup_sales.clj:8-12
  4. Migration pollutes live app bufferssales_to_parquet.clj writes to the global p/*buffers*. Running migration alongside live traffic mixes historical with new records.

    • src/clj/auto_ap/migration/sales_to_parquet.clj:160
  5. Dead code: get-fees defined twice — Private at line 148, public shadowing copy at line 193.

    • src/clj/auto_ap/jobs/sales_summaries.clj:148,193
  6. Old Datomic query functions left alongside parquet versionsget-tax, get-tip, get-sales, get-refunds (Datomic versions at lines 201-273) are dead after migration but still loadable.

    • src/clj/auto_ap/jobs/sales_summaries.clj:201+
  7. WAL append not atomic under concurrency — Multiple buffer! calls to the same entity-type .jsonl file can interleave bytes in multi-threaded server context.

    • src/clj/auto_ap/storage/parquet.clj:105-114

P3 — Low (Minor)

18. Test coverage: 5 tests for ~800 lines of new code

test/clj/auto_ap/storage/parquet_test.clj · testing

The existing 5 tests cover only query-scalar, buffer!, clear-buffer!, and date-seq. Missing critical paths:

  • No flush-to-parquet round trip test (write → flush → query back)
  • No WAL recovery test (seed JSONL file, call load-unflushed!, verify data recovered)
  • No integration test for parquet aggregation (sales_summaries.clj functions return correct results after imports go through parquet path)
  • No test coverage for any function in sales_orders_new.clj

19. Naming inconsistency

buffer-count, total-buf-count, get-unflushed-count — three names for conceptually similar metrics. Unify to buffer-count (per-type) and total-buffer-count (all).

  • src/clj/auto_ap/storage/parquet.clj:120,123,195

Summary

Severity Count Status
P0 4 🚫 Block merge — must fix
P1 6 ⚠️ Fix before ship
P2 7 🔧 Consider for next iteration or address now
P3 2 📝 Nice to have

Top blocking items: The sales_orders_new.clj namespace collision (finding #1) and the safe-cleanup-all destructure bug (#2) are the most consequential — they can cause data loss in production before anyone notices. SQL injection (#3) is exploitable if filter values reach GraphQL endpoints without sanitization.

Positive notes:

  • Good WAL recovery design for crash resilience
  • S3 verification gating on cleanup is the right safety pattern (when it works correctly)
  • Parquet+S3+DuckDB architecture aligns with plan well
# Code Review: feat/sales-parquet-migration **Scope:** 15 files, ~2200 lines added (base: `297464c1`, HEAD: `cc838adf`) **Reviewers:** correctness, testing, maintainability, project-standards, agent-native, learnings **Plan coverage:** R1-R5 all addressed across U1-U7. Plan file at `docs/plans/2026-04-24-001-refactor-detailed-sales-to-parquet-plan.md`. --- ## P0 — Critical (Must fix before merge) ### 1. `sales_orders_new.clj` namespace collision — may break ALL sales queries **`src/clj/auto_ap/datomic/sales_orders_new.clj:1`** · correctness, maintainability Both `sales_orders.clj` and the new `sales_orders_new.clj` declare `(ns auto-ap.datomic.sales-orders)` — the **same namespace**. Depending on classpath load order, one silently shadows the other. The old file is already wired to use parquet (`:require [auto-ap.storage.parquet :as pq]`, see lines 4, 56). The new file is **also broken internally**: - `raw-graphql-ids` (line 157) threads from `nil` → `pq/get-sales-orders` but discards the result, then references unbound `result` at line 165 - Missing `:require [clojure.string :as str]` despite using `str/join` at line 52 — won't compile - Missing `clojure.set` and `com.brunobonacci.mulog` requires - `get-graphql` (line 209) references undefined `id-keys` and `matching-count` **Suggested fix:** Delete `sales_orders_new.clj`. The existing `sales_orders.clj` has been correctly updated to use the parquet read path. If you intended to replace it, rename the new namespace and fix all undefined references before landing. ### 2. `safe-cleanup-all` destructure bug — passes wrong year/month, deletes unverified data **`src/clj/auto_ap/migration/cleanup_sales.clj:199`** · correctness ```clojure ; collect-all-months returns [[2024 3] [2024 4] ...] (pairs of [year month]) (doseq [[_ y m] months] ; ← drops _=year, binds y=month, m=nil ... (verify-month-in-s3? y m) ; → (verify-month-in-s3? 3 nil) — wrong year, nil month!) ``` The destructure pattern `[_ y m]` against a 2-element `[year month]` vector discards the year and binds `nil` to month. Both S3 verification and delete calls receive garbage arguments. **Suggested fix:** Change `[_ y m]` → `[y m]`. ### 3. SQL injection in WHERE clause construction **`src/clj/auto_ap/storage/parquet.clj:236`**, **`sales_summaries.clj` (all aggregation functions)** · correctness, maintainability User-supplied filter values (`:client`, `:vendor`, `:location`) are concatenated directly into SQL strings: ```clojure (str env " = '" v "'") ; no escaping — single quotes, semicolons, or keywords break the query ``` **Suggested fix:** At minimum, escape single quotes by doubling them: `(str/replace v "'" "''")`. Long term, use DuckDB PreparedStatement with parameter binding. ### 4. `query-deduped` generates invalid SQL **`src/clj/auto_ap/storage/parquet.clj:279-284`** · correctness ```clojure (str ":sql q) " QUALIFY ROW_NUMBER() OVER" ; space breaks QUALIFY from expression " (PARTITION BY sales_order.external_id" ; column doesn't exist in parquet schema " ORDER BY _seq_no DESC) = 1")) ``` The generated SQL has a space between `QUALIFY` and `ROW_NUMBER()` breaking the syntax. The partition column `sales_order.external_id` doesn't exist — parquet records use `external_id`. Function always fails at runtime. **Suggested fix:** Remove leading space, correct column name: `" QUALIFY ROW_NUMBER() OVER (PARTITION BY external_id ORDER BY _seq_no DESC) = 1"`. --- ## P1 — High (Should fix) ### 5. DuckDB shutdown hook is a no-op **`src/clj/auto_ap/storage/parquet.clj:26-27`** · correctness, maintainability `(Thread. #(fn []))` creates a thread that returns an unevaluated function and never calls `disconnect!`. DuckDB connections persist until JVM crash. **Fix:** `(Thread. #(.close ^java.sql.Connection @db))` ### 6. Query results/statements leak — not closed in `with-open` **`src/clj/auto_ap/storage/parquet.clj:53-73`** · correctness, performance Neither `query-scalar` nor `query-rows` close their `Statement` or `ResultSet` objects. Under sustained load this exhausts DuckDB internal cursors and file handles. **Fix:** Wrap in `with-open [stmt (.createStatement conn) rs (.executeQuery stmt sql)] ...` ### 7. `flush-to-parquet!` clears buffer before S3 upload completes **`src/clj/auto_ap/storage/parquet.clj:148`** · correctness, maintainability Buffer is cleared at line 148, then deleted files at 149-150. But `(upload-parquet! ...)` runs before the clear. On upload failure the catch throws — but if an unexpected exception occurs between upload success and buffer clear, in-memory records are lost (WAL has them, but they won't re-flush until restart). **Fix:** Use explicit ordering: verify S3 200 response → confirm file persisted → THEN clear buffer. ### 8. `get-payment-items-parquet` key mismatch — silently returns empty payment data **`src/clj/auto_ap/jobs/sales_summaries.clj:106`** · correctness ```clojure (filter #(= client-code (:client_code %)) rows) ; ← underscored key :client_code ; but parquet records are written with (:client-code ...) — hyphenated key ``` The filter looks for `:client_code` (underscore), but all write paths (`ezcater/core.clj`, `square/core3.clj`, `sales_to_parquet.clj`) store as `:client-code`. Payment aggregation silently returns zero across all clients. **Fix:** Change `:client_code` → `:client-code`. ### 9. `date-seq` produces forward sequence when `start > end` **`src/clj/auto_ap/storage/parquet.clj:203-211`** · correctness `(Math/abs ...)` on the epoch diff means reversing arguments still produces a valid-looking sequence going forward from start by |diff| days. Queries against non-existent S3 keys silently return empty results. **Fix:** Assert `start <= end` or reverse iteration direction based on comparison. ### 10. Entity-type array duplicated in 5+ locations **parquet.clj (×2), sales_to_parquet.clj (×2), cleanup_sales.clj (×1)** · maintainability, testing The `["sales-order" "charge" "line-item" "sales-refund"]` vector is hardcoded in five places with no single source of truth. Adding or renaming an entity type risks silent data gaps. **Fix:** Define `ENTITY-TYPES` once in `auto-ap.storage.parquet` and require it elsewhere. --- ## P2 — Moderate (Fix if straightforward) 11. **`object-exists?` downloads full S3 objects** — Use `s3/head-object` instead of `s3/get-object`. Downloads entire parquet files just to check existence (~3000+ GET requests per cleanup run). - `src/clj/auto_ap/migration/cleanup_sales.clj:113-121` 12. **Perf test runs at namespace load** — `(run-perf-tests)` at line 111 triggers 100k row generation and S3 upload on every `lein test`. Wrap in `deftest` or `(comment ...)`. - `test/clj/auto_ap/storage/perf_test.clj:111` 13. **DRY-RUN? mutable var, not thread-safe** — Global boolean toggled with `alter-var-root`. Concurrent invocations race. - `src/clj/auto_ap/migration/cleanup_sales.clj:8-12` 14. **Migration pollutes live app buffers** — `sales_to_parquet.clj` writes to the global `p/*buffers*`. Running migration alongside live traffic mixes historical with new records. - `src/clj/auto_ap/migration/sales_to_parquet.clj:160` 15. **Dead code: `get-fees` defined twice** — Private at line 148, public shadowing copy at line 193. - `src/clj/auto_ap/jobs/sales_summaries.clj:148,193` 16. **Old Datomic query functions left alongside parquet versions** — `get-tax`, `get-tip`, `get-sales`, `get-refunds` (Datomic versions at lines 201-273) are dead after migration but still loadable. - `src/clj/auto_ap/jobs/sales_summaries.clj:201+` 17. **WAL append not atomic under concurrency** — Multiple `buffer!` calls to the same entity-type .jsonl file can interleave bytes in multi-threaded server context. - `src/clj/auto_ap/storage/parquet.clj:105-114` --- ## P3 — Low (Minor) ### 18. Test coverage: 5 tests for ~800 lines of new code **`test/clj/auto_ap/storage/parquet_test.clj`** · testing The existing 5 tests cover only `query-scalar`, `buffer!`, `clear-buffer!`, and `date-seq`. Missing critical paths: - No flush-to-parquet round trip test (write → flush → query back) - No WAL recovery test (seed JSONL file, call `load-unflushed!`, verify data recovered) - No integration test for parquet aggregation (`sales_summaries.clj` functions return correct results after imports go through parquet path) - No test coverage for **any** function in `sales_orders_new.clj` ### 19. Naming inconsistency `buffer-count`, `total-buf-count`, `get-unflushed-count` — three names for conceptually similar metrics. Unify to `buffer-count` (per-type) and `total-buffer-count` (all). - `src/clj/auto_ap/storage/parquet.clj:120,123,195` --- ## Summary | Severity | Count | Status | |----------|-------|--------| | P0 | 4 | 🚫 Block merge — must fix | | P1 | 6 | ⚠️ Fix before ship | | P2 | 7 | 🔧 Consider for next iteration or address now | | P3 | 2 | 📝 Nice to have | **Top blocking items:** The `sales_orders_new.clj` namespace collision (finding #1) and the `safe-cleanup-all` destructure bug (#2) are the most consequential — they can cause data loss in production before anyone notices. SQL injection (#3) is exploitable if filter values reach GraphQL endpoints without sanitization. **Positive notes:** - Good WAL recovery design for crash resilience - S3 verification gating on cleanup is the right safety pattern (when it works correctly) - Parquet+S3+DuckDB architecture aligns with plan well
notid added 4 commits 2026-04-27 20:05:37 -07:00
- Add DuckDB/S3 parquet storage layer (auto-ap.storage.parquet)
- Add sales_to_parquet migration script for historical data
- Add cleanup_sales for post-migration Datomic cleanup
- Add sales_orders_new.clj with DuckDB read layer for SSR views
- Add test scaffolding for parquet storage
- Add plan document for move-detailed-sales-to-parquet

feat(sales): redirect production and read flows to Parquet/DuckDB

- U3: Square production (upsert) now buffers to parquet via flatten-order-to-parquet!
- U3: EzCater core import-order now buffers to parquet instead of Datomic transact
- U3: EzCater XLS upload-xls now buffers to parquet instead of audit-transact
- U4: Rewrite sales_orders.clj to read from DuckDB via pq/get-sales-orders
- U5: Rewrite sales_summaries to use parquet aggregation functions
  - get-payment-items-parquet, get-discounts-parquet, get-refund-items-parquet
  - get-tax-parquet, get-tip-parquet, get-sales-parquet
- Add sum-* aggregation functions to storage/sales_summaries.clj
  - sum-discounts, sum-refunds-by-type, sum-taxes, sum-tips, sum-sales-by-category
- Remove sales_orders_new.clj (unreferenced, duplicate ns)
- Add [clojure.string :as str] to sales_orders.clj ns
- Fix double ORDER BY in sales_orders raw-graphql-ids (was passing full
  ORDER BY clause from build-sort-clause into get-sales-orders which
  prepends its own ORDER BY, producing 'ORDER BY ORDER BY ... DESC DESC')
- Fix WHERE clause column names in parquet build-where-clause:
  external_id.client -> client-code, external_id.vendor -> vendor
- Fix parquet-query format string (%%s -> %s with proper format call)
- Fix ex-info call signature in flush! (was passing :error as third arg
  instead of inside the data map)
- Add S3 credentials to DuckDB connect! so httpfs can read from S3
- Fix parquet buffer indentation and alignment across square/core3,
  ezcater/core, ezcater_xls, payments, sales_summaries, migrations
- Fix broken Datomic query syntax in ezcater/core (upsert-used-subscriptions,
  upsert-recent find/where clauses mangled by paren-repair)
- Uncomment accidentally commented code block in square/core3
- Fix paren/indentation issues in ssr/payments, jobs/sales_summaries
- Add fetch-page-ssr and summarize-page-ssr to read from parquet via DuckDB
- Add get-sales-orders-summary for cross-page totals (SUM across all rows)
- Optimize parquet-query for large ranges (>60 days) with year-level globs
- Add default-date-range with fallback to data's actual range
- Fix migration: flatten-order-to-pieces! vswap!, pull specs, date handling
- Add denormalized columns: payment-methods, processors, categories, source
- Handle schema-enforce middleware stripping dates via raw query-string parsing
- Add graceful fallback for missing parquet files (catch Exception)
- Fix load-unflushed! with .exists check on WAL files
notid force-pushed feat/sales-parquet-migration from cc838adfac to 9153494ed7 2026-04-27 20:05:37 -07:00 Compare
notid added 1 commit 2026-04-28 21:09:29 -07:00
- Cache COUNT queries and summary queries with LRU (256) + TTL (30 min) caches
- Pass session client codes to parquet queries via IN clause (was showing all clients)
- Normalize MM/dd/yyyy date strings from UI in parquet.clj (month-seq, parquet-query)
- Remove expensive get-sales-orders-summary call from default-date-range
- Add mu/trace and mu/log throughout parquet query layer
notid added 1 commit 2026-04-28 21:54:49 -07:00
- Fix pq-files using date-seq (daily) vs actual monthly parquet partitions
- Fix safe-cleanup-all destructure [[_ y m]] -> [[y m]] against [year month]
- Fix shutdown hook no-op: Thread wrapping #(fn []) now actually closes conn
- Fix query-deduped: PARTITION BY "external-id" not sales_order.external_id
- Fix :client_code -> :client-code key mismatch in get-payment-items-parquet
- Fix object-exists? downloading full S3 objects; use head-object instead
- Fix date-seq silently producing wrong range when start > end; now throws
- Remove duplicate private get-fees that shadowed public version
- Deduplicate date-seq: remove from sales_to_parquet, use p/date-seq
- Wrap run-perf-tests in (comment ...) to prevent execution on lein test
- Make month-seq public so sales_summaries.clj can use it
Checking for merge conflicts…
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/sales-parquet-migration:feat/sales-parquet-migration
git checkout feat/sales-parquet-migration
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: notid/integreat#3