├── .github └── workflows │ ├── docker.yml │ └── release.yml ├── 2.18.1.patch ├── README.md └── examples ├── docker-compose.yml └── umami-pod.yml /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker image 2 | run-name: Create and publish Docker image 3 | on: workflow_dispatch 4 | 5 | permissions: 6 | contents: read 7 | packages: write 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | platform: 14 | - linux/amd64 15 | - linux/arm64 16 | runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} 17 | steps: 18 | - name: Prepare 19 | run: | 20 | platform=${{ matrix.platform }} 21 | echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV 22 | echo "REGISTRY_IMAGE=ghcr.io/${GITHUB_REPOSITORY@L}" >> $GITHUB_ENV 23 | 24 | - name: Download Umami with patch applied 25 | env: 26 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | run: | 28 | gh release download --repo $GITHUB_REPOSITORY --pattern umami-sqlite.zip 29 | unzip -q umami-sqlite.zip && rm umami-sqlite.zip 30 | UMAMI_FOLDER=$(ls -d umami-*) 31 | mv $UMAMI_FOLDER umami 32 | 33 | - name: Login to GitHub Container Registry 34 | uses: docker/login-action@v3 35 | with: 36 | registry: ghcr.io 37 | username: ${{ github.actor }} 38 | password: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | - name: Set up Docker Buildx 41 | uses: docker/setup-buildx-action@v3 42 | 43 | - name: Build and push by digest 44 | id: build 45 | env: 46 | DOCKER_BUILD_SUMMARY: false 47 | DOCKER_BUILD_RECORD_UPLOAD: false 48 | uses: docker/build-push-action@v6 49 | with: 50 | context: ./umami 51 | build-args: DATABASE_TYPE=file 52 | tags: ${{ env.REGISTRY_IMAGE }} 53 | platforms: ${{ matrix.platform }} 54 | outputs: type=image,push=true,push-by-digest=true 55 | provenance: false 56 | 57 | - name: Export digest 58 | run: | 59 | mkdir -p ${{ runner.temp }}/digests 60 | digest="${{ steps.build.outputs.digest }}" 61 | touch "${{ runner.temp }}/digests/${digest#sha256:}" 62 | 63 | - name: Upload digest 64 | uses: actions/upload-artifact@v4 65 | with: 66 | name: digest-${{ env.PLATFORM_PAIR }} 67 | path: ${{ runner.temp }}/digests/* 68 | if-no-files-found: error 69 | retention-days: 1 70 | 71 | merge: 72 | runs-on: ubuntu-latest 73 | needs: 74 | - build 75 | steps: 76 | - name: Prepare 77 | run: echo "REGISTRY_IMAGE=ghcr.io/${GITHUB_REPOSITORY@L}" >> $GITHUB_ENV 78 | 79 | - name: Download digests 80 | uses: actions/download-artifact@v4 81 | with: 82 | path: ${{ runner.temp }}/digests 83 | pattern: digest-* 84 | merge-multiple: true 85 | 86 | - name: Login to GitHub Container Registry 87 | uses: docker/login-action@v3 88 | with: 89 | registry: ghcr.io 90 | username: ${{ github.actor }} 91 | password: ${{ secrets.GITHUB_TOKEN }} 92 | 93 | - name: Create manifest list and push 94 | working-directory: ${{ runner.temp }}/digests 95 | run: | 96 | docker manifest create ${REGISTRY_IMAGE}:latest \ 97 | $(printf -- "--amend ${REGISTRY_IMAGE}@sha256:%s " *) 98 | docker manifest push ${REGISTRY_IMAGE}:latest 99 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | run-name: Patch and release 3 | on: workflow_dispatch 4 | 5 | permissions: 6 | contents: write 7 | 8 | env: 9 | RELEASE_FILE: umami-sqlite.zip 10 | 11 | jobs: 12 | create-release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Prepare 19 | run: | 20 | echo 'work' > .gitignore 21 | mkdir work 22 | 23 | - name: Get Umami version 24 | run: | 25 | PATCH_FILE=$(ls *.patch) 26 | echo "UMAMI_VERSION=v${PATCH_FILE%.*}" >> $GITHUB_ENV 27 | 28 | - name: Download Umami 29 | run: | 30 | cd work 31 | wget https://github.com/umami-software/umami/archive/refs/tags/$UMAMI_VERSION.zip 32 | unzip -q $UMAMI_VERSION.zip 33 | 34 | - name: Apply patch and zip 35 | run: | 36 | cd work 37 | UMAMI_FOLDER=$(ls -d umami-*) 38 | cd $UMAMI_FOLDER 39 | patch -p1 < ../../*.patch 40 | cd .. 41 | zip -q -r $RELEASE_FILE $UMAMI_FOLDER 42 | 43 | - name: Create release 44 | env: 45 | GH_TOKEN: ${{ github.token }} 46 | run: gh release create $UMAMI_VERSION work/$RELEASE_FILE --generate-notes --draft 47 | -------------------------------------------------------------------------------- /2.18.1.patch: -------------------------------------------------------------------------------- 1 | --- 2 | Dockerfile | 3 + 3 | db/sqlite/migrations/01_init/migration.sql | 127 ++++++++++ 4 | .../02_1_unixepoch_availability/migration.sql | 15 ++ 5 | .../migration.sql | 49 ++++ 6 | .../03_metric_performance_index/migration.sql | 50 ++++ 7 | .../04_1_indexes_tables/migration.sql | 206 ++++++++++++++++ 8 | .../migrations/04_team_redesign/migration.sql | 31 +++ 9 | .../migrations/05_add_visit_id/migration.sql | 58 +++++ 10 | .../migrations/06_session_data/migration.sql | 20 ++ 11 | db/sqlite/migrations/07_add_tag/migration.sql | 5 + 12 | .../migrations/08_1_datetime/migration.sql | 145 +++++++++++ 13 | .../migrations/08_add_utm_clid/migration.sql | 12 + 14 | .../09_update_hostname_region/migration.sql | 22 ++ 15 | .../10_add_distinct_id/migration.sql | 5 + 16 | db/sqlite/migrations/migration_lock.toml | 3 + 17 | db/sqlite/schema.prisma | 231 ++++++++++++++++++ 18 | scripts/check-db.js | 33 ++- 19 | scripts/copy-db-files.js | 6 +- 20 | scripts/sqlite/convert-utm-clid-columns.js | 62 +++++ 21 | scripts/sqlite/vacuum.js | 10 + 22 | src/lib/db.ts | 17 +- 23 | src/lib/prisma.ts | 58 ++++- 24 | src/queries/prisma/team.ts | 5 +- 25 | src/queries/sql/events/getWebsiteEvents.ts | 4 +- 26 | src/queries/sql/getRealtimeActivity.ts | 4 +- 27 | src/queries/sql/getWebsiteDateRange.ts | 6 +- 28 | src/queries/sql/sessions/getSessionData.ts | 6 +- 29 | src/queries/sql/sessions/getWebsiteSession.ts | 6 +- 30 | .../sql/sessions/getWebsiteSessions.ts | 8 +- 31 | 29 files changed, 1163 insertions(+), 44 deletions(-) 32 | create mode 100644 db/sqlite/migrations/01_init/migration.sql 33 | create mode 100644 db/sqlite/migrations/02_1_unixepoch_availability/migration.sql 34 | create mode 100644 db/sqlite/migrations/02_report_schema_session_data/migration.sql 35 | create mode 100644 db/sqlite/migrations/03_metric_performance_index/migration.sql 36 | create mode 100644 db/sqlite/migrations/04_1_indexes_tables/migration.sql 37 | create mode 100644 db/sqlite/migrations/04_team_redesign/migration.sql 38 | create mode 100644 db/sqlite/migrations/05_add_visit_id/migration.sql 39 | create mode 100644 db/sqlite/migrations/06_session_data/migration.sql 40 | create mode 100644 db/sqlite/migrations/07_add_tag/migration.sql 41 | create mode 100644 db/sqlite/migrations/08_1_datetime/migration.sql 42 | create mode 100644 db/sqlite/migrations/08_add_utm_clid/migration.sql 43 | create mode 100644 db/sqlite/migrations/09_update_hostname_region/migration.sql 44 | create mode 100644 db/sqlite/migrations/10_add_distinct_id/migration.sql 45 | create mode 100644 db/sqlite/migrations/migration_lock.toml 46 | create mode 100644 db/sqlite/schema.prisma 47 | create mode 100644 scripts/sqlite/convert-utm-clid-columns.js 48 | create mode 100644 scripts/sqlite/vacuum.js 49 | 50 | diff --git a/Dockerfile b/Dockerfile 51 | index 4b15664..c3ddf4c 100644 52 | --- a/Dockerfile 53 | +++ b/Dockerfile 54 | @@ -58,6 +58,9 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 55 | # Custom routes 56 | RUN mv ./.next/routes-manifest.json ./.next/routes-manifest-orig.json 57 | 58 | +RUN mkdir /db \ 59 | + && chown nextjs:nodejs /db 60 | + 61 | USER nextjs 62 | 63 | EXPOSE 3000 64 | diff --git a/db/sqlite/migrations/01_init/migration.sql b/db/sqlite/migrations/01_init/migration.sql 65 | new file mode 100644 66 | index 0000000..57c1fd1 67 | --- /dev/null 68 | +++ b/db/sqlite/migrations/01_init/migration.sql 69 | @@ -0,0 +1,127 @@ 70 | +-- CreateTable 71 | +CREATE TABLE `user` ( 72 | + `user_id` TEXT PRIMARY KEY NOT NULL, 73 | + `username` TEXT NOT NULL, 74 | + `password` TEXT NOT NULL, 75 | + `role` TEXT NOT NULL, 76 | + `created_at` INTEGER NULL DEFAULT (strftime('%s', 'now')), 77 | + `updated_at` INTEGER NULL, 78 | + `deleted_at` INTEGER NULL 79 | +); 80 | +CREATE UNIQUE INDEX `user_user_id_key` ON `user`(`user_id`); 81 | +CREATE UNIQUE INDEX `user_username_key` ON `user`(`username`); 82 | + 83 | +-- CreateTable 84 | +CREATE TABLE `session` ( 85 | + `session_id` TEXT PRIMARY KEY NOT NULL, 86 | + `website_id` TEXT NOT NULL, 87 | + `hostname` TEXT NULL, 88 | + `browser` TEXT NULL, 89 | + `os` TEXT NULL, 90 | + `device` TEXT NULL, 91 | + `screen` TEXT NULL, 92 | + `language` TEXT NULL, 93 | + `country` TEXT NULL, 94 | + `subdivision1` TEXT NULL, 95 | + `subdivision2` TEXT NULL, 96 | + `city` TEXT NULL, 97 | + `created_at` INTEGER NULL DEFAULT (strftime('%s', 'now')) 98 | +); 99 | +CREATE UNIQUE INDEX `session_session_id_key` ON `session`(`session_id`); 100 | +CREATE INDEX `session_created_at_idx` ON `session`(`created_at`); 101 | +CREATE INDEX `session_website_id_idx` ON `session`(`website_id`); 102 | + 103 | +-- CreateTable 104 | +CREATE TABLE `website` ( 105 | + `website_id` TEXT PRIMARY KEY NOT NULL, 106 | + `name` TEXT NOT NULL, 107 | + `domain` TEXT NULL, 108 | + `share_id` TEXT NULL, 109 | + `reset_at` INTEGER NULL, 110 | + `user_id` TEXT NULL, 111 | + `created_at` INTEGER NULL DEFAULT (strftime('%s', 'now')), 112 | + `updated_at` INTEGER NULL, 113 | + `deleted_at` INTEGER NULL 114 | +); 115 | +CREATE UNIQUE INDEX `website_website_id_key` ON `website`(`website_id`); 116 | +CREATE UNIQUE INDEX `website_share_id_key` ON `website`(`share_id`); 117 | +CREATE INDEX `website_user_id_idx` ON `website`(`user_id`); 118 | +CREATE INDEX `website_created_at_idx` ON `website`(`created_at`); 119 | +CREATE INDEX `website_share_id_idx` ON `website`(`share_id`); 120 | + 121 | +-- CreateTable 122 | +CREATE TABLE `website_event` ( 123 | + `event_id` TEXT PRIMARY KEY NOT NULL, 124 | + `website_id` TEXT NOT NULL, 125 | + `session_id` TEXT NOT NULL, 126 | + `created_at` INTEGER NULL DEFAULT (strftime('%s', 'now')), 127 | + `url_path` TEXT NOT NULL, 128 | + `url_query` TEXT NULL, 129 | + `referrer_path` TEXT NULL, 130 | + `referrer_query` TEXT NULL, 131 | + `referrer_domain` TEXT NULL, 132 | + `page_title` TEXT NULL, 133 | + `event_type` INTEGER UNSIGNED NOT NULL DEFAULT 1, 134 | + `event_name` TEXT NULL 135 | +); 136 | +CREATE INDEX `website_event_created_at_idx` ON `website_event`(`created_at`); 137 | +CREATE INDEX `website_event_session_id_idx` ON `website_event`(`session_id`); 138 | +CREATE INDEX `website_event_website_id_idx` ON `website_event`(`website_id`); 139 | +CREATE INDEX `website_event_website_id_created_at_idx` ON `website_event`(`website_id`, `created_at`); 140 | +CREATE INDEX `website_event_website_id_session_id_created_at_idx` ON `website_event`(`website_id`, `session_id`, `created_at`); 141 | + 142 | +-- CreateTable 143 | +CREATE TABLE `event_data` ( 144 | + `event_id` TEXT PRIMARY KEY NOT NULL, 145 | + `website_event_id` TEXT NOT NULL, 146 | + `website_id` TEXT NOT NULL, 147 | + `event_key` TEXT NOT NULL, 148 | + `event_string_value` TEXT NULL, 149 | + `event_numeric_value` NUMERIC NULL, 150 | + `event_date_value` INTEGER NULL, 151 | + `event_data_type` INTEGER UNSIGNED NOT NULL, 152 | + `created_at` INTEGER NULL DEFAULT (strftime('%s', 'now')) 153 | +); 154 | +CREATE INDEX `event_data_created_at_idx` ON `event_data`(`created_at`); 155 | +CREATE INDEX `event_data_website_id_idx` ON `event_data`(`website_id`); 156 | +CREATE INDEX `event_data_website_event_id_idx` ON `event_data`(`website_event_id`); 157 | +CREATE INDEX `event_data_website_id_website_event_id_created_at_idx` ON `event_data`(`website_id`, `website_event_id`, `created_at`); 158 | + 159 | +-- CreateTable 160 | +CREATE TABLE `team` ( 161 | + `team_id` TEXT PRIMARY KEY NOT NULL, 162 | + `name` TEXT NOT NULL, 163 | + `access_code` TEXT NULL, 164 | + `created_at` INTEGER NULL DEFAULT (strftime('%s', 'now')), 165 | + `updated_at` INTEGER NULL 166 | +); 167 | +CREATE UNIQUE INDEX `team_team_id_key` ON `team`(`team_id`); 168 | +CREATE UNIQUE INDEX `team_access_code_key` ON `team`(`access_code`); 169 | +CREATE INDEX `team_access_code_idx` ON `team`(`access_code`); 170 | + 171 | +-- CreateTable 172 | +CREATE TABLE `team_user` ( 173 | + `team_user_id` TEXT PRIMARY KEY NOT NULL, 174 | + `team_id` TEXT NOT NULL, 175 | + `user_id` TEXT NOT NULL, 176 | + `role` TEXT NOT NULL, 177 | + `created_at` INTEGER NULL DEFAULT (strftime('%s', 'now')), 178 | + `updated_at` INTEGER NULL 179 | +); 180 | +CREATE UNIQUE INDEX `team_user_team_user_id_key` ON `team_user`(`team_user_id`); 181 | +CREATE INDEX `team_user_team_id_idx` ON `team_user`(`team_id`); 182 | +CREATE INDEX `team_user_user_id_idx` ON `team_user`(`user_id`); 183 | + 184 | +-- CreateTable 185 | +CREATE TABLE `team_website` ( 186 | + `team_website_id` TEXT PRIMARY KEY NOT NULL, 187 | + `team_id` TEXT NOT NULL, 188 | + `website_id` TEXT NOT NULL, 189 | + `created_at` INTEGER NULL DEFAULT (strftime('%s', 'now')) 190 | +); 191 | +CREATE UNIQUE INDEX `team_website_team_website_id_key` ON `team_website`(`team_website_id`); 192 | +CREATE INDEX `team_website_team_id_idx` ON `team_website`(`team_id`); 193 | +CREATE INDEX `team_website_website_id_idx` ON `team_website`(`website_id`); 194 | + 195 | +-- AddSystemUser 196 | +INSERT INTO user (user_id, username, role, password) VALUES ('41e2b680-648e-4b09-bcd7-3e2b10c06264' , 'admin', 'admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa'); 197 | \ No newline at end of file 198 | diff --git a/db/sqlite/migrations/02_1_unixepoch_availability/migration.sql b/db/sqlite/migrations/02_1_unixepoch_availability/migration.sql 199 | new file mode 100644 200 | index 0000000..4123bbd 201 | --- /dev/null 202 | +++ b/db/sqlite/migrations/02_1_unixepoch_availability/migration.sql 203 | @@ -0,0 +1,15 @@ 204 | +-- Update defaults and force schema_version incrementation with an immediatly dropped new table 205 | +BEGIN TRANSACTION; 206 | +PRAGMA writable_schema=ON; 207 | +UPDATE sqlite_schema SET sql='CREATE TABLE `user` ( `user_id` TEXT PRIMARY KEY NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `role` TEXT NOT NULL, `created_at` INTEGER NULL DEFAULT (unixepoch()), `updated_at` INTEGER NULL, `deleted_at` INTEGER NULL )' WHERE type='table' AND name='user'; 208 | +UPDATE sqlite_schema SET sql='CREATE TABLE `session` ( `session_id` TEXT PRIMARY KEY NOT NULL, `website_id` TEXT NOT NULL, `hostname` TEXT NULL, `browser` TEXT NULL, `os` TEXT NULL, `device` TEXT NULL, `screen` TEXT NULL, `language` TEXT NULL, `country` TEXT NULL, `subdivision1` TEXT NULL, `subdivision2` TEXT NULL, `city` TEXT NULL, `created_at` INTEGER NULL DEFAULT (unixepoch()) )' WHERE type='table' AND name='session'; 209 | +UPDATE sqlite_schema SET sql='CREATE TABLE `website` ( `website_id` TEXT PRIMARY KEY NOT NULL, `name` TEXT NOT NULL, `domain` TEXT NULL, `share_id` TEXT NULL, `reset_at` INTEGER NULL, `user_id` TEXT NULL, `created_at` INTEGER NULL DEFAULT (unixepoch()), `updated_at` INTEGER NULL, `deleted_at` INTEGER NULL )' WHERE type='table' AND name='website'; 210 | +UPDATE sqlite_schema SET sql='CREATE TABLE `website_event` ( `event_id` TEXT PRIMARY KEY NOT NULL, `website_id` TEXT NOT NULL, `session_id` TEXT NOT NULL, `created_at` INTEGER NULL DEFAULT (unixepoch()), `url_path` TEXT NOT NULL, `url_query` TEXT NULL, `referrer_path` TEXT NULL, `referrer_query` TEXT NULL, `referrer_domain` TEXT NULL, `page_title` TEXT NULL, `event_type` INTEGER UNSIGNED NOT NULL DEFAULT 1, `event_name` TEXT NULL )' WHERE type='table' AND name='website_event'; 211 | +UPDATE sqlite_schema SET sql='CREATE TABLE `event_data` ( `event_id` TEXT PRIMARY KEY NOT NULL, `website_event_id` TEXT NOT NULL, `website_id` TEXT NOT NULL, `event_key` TEXT NOT NULL, `event_string_value` TEXT NULL, `event_numeric_value` NUMERIC NULL, `event_date_value` INTEGER NULL, `event_data_type` INTEGER UNSIGNED NOT NULL, `created_at` INTEGER NULL DEFAULT (unixepoch()) )' WHERE type='table' AND name='event_data'; 212 | +UPDATE sqlite_schema SET sql='CREATE TABLE `team` ( `team_id` TEXT PRIMARY KEY NOT NULL, `name` TEXT NOT NULL, `access_code` TEXT NULL, `created_at` INTEGER NULL DEFAULT (unixepoch()), `updated_at` INTEGER NULL )' WHERE type='table' AND name='team'; 213 | +UPDATE sqlite_schema SET sql='CREATE TABLE `team_user` ( `team_user_id` TEXT PRIMARY KEY NOT NULL, `team_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `role` TEXT NOT NULL, `created_at` INTEGER NULL DEFAULT (unixepoch()), `updated_at` INTEGER NULL )' WHERE type='table' AND name='team_user'; 214 | +UPDATE sqlite_schema SET sql='CREATE TABLE `team_website` ( `team_website_id` TEXT PRIMARY KEY NOT NULL, `team_id` TEXT NOT NULL, `website_id` TEXT NOT NULL, `created_at` INTEGER NULL DEFAULT (unixepoch()) )' WHERE type='table' AND name='team_website'; 215 | +CREATE TABLE update_schema_version ( version INTEGER ); 216 | +DROP TABLE update_schema_version; 217 | +PRAGMA writable_schema=OFF; 218 | +COMMIT; 219 | \ No newline at end of file 220 | diff --git a/db/sqlite/migrations/02_report_schema_session_data/migration.sql b/db/sqlite/migrations/02_report_schema_session_data/migration.sql 221 | new file mode 100644 222 | index 0000000..9ee9975 223 | --- /dev/null 224 | +++ b/db/sqlite/migrations/02_report_schema_session_data/migration.sql 225 | @@ -0,0 +1,49 @@ 226 | +-- AlterTable 227 | +ALTER TABLE `event_data` RENAME COLUMN `event_data_type` TO `data_type`; 228 | +ALTER TABLE `event_data` RENAME COLUMN `event_date_value` TO `date_value`; 229 | +ALTER TABLE `event_data` RENAME COLUMN `event_id` TO `event_data_id`; 230 | +ALTER TABLE `event_data` RENAME COLUMN `event_numeric_value` TO `number_value`; 231 | +ALTER TABLE `event_data` RENAME COLUMN `event_string_value` TO `string_value`; 232 | + 233 | +-- CreateTable 234 | +CREATE TABLE `session_data` ( 235 | + `session_data_id` TEXT PRIMARY KEY NOT NULL, 236 | + `website_id` TEXT NOT NULL, 237 | + `session_id` TEXT NOT NULL, 238 | + `event_key` TEXT NOT NULL, 239 | + `string_value` TEXT NULL, 240 | + `number_value` NUMERIC NULL, 241 | + `date_value` INTEGER NULL, 242 | + `data_type` INTEGER UNSIGNED NOT NULL, 243 | + `created_at` INTEGER NULL DEFAULT (unixepoch()) 244 | +); 245 | +CREATE INDEX `session_data_created_at_idx` ON `session_data`(`created_at`); 246 | +CREATE INDEX `session_data_website_id_idx` ON `session_data`(`website_id`); 247 | +CREATE INDEX `session_data_session_id_idx` ON `session_data`(`session_id`); 248 | + 249 | +-- CreateTable 250 | +CREATE TABLE `report` ( 251 | + `report_id` TEXT PRIMARY KEY NOT NULL, 252 | + `user_id` TEXT NOT NULL, 253 | + `website_id` TEXT NOT NULL, 254 | + `type` TEXT NOT NULL, 255 | + `name` TEXT NOT NULL, 256 | + `description` TEXT NOT NULL, 257 | + `parameters` TEXT NOT NULL, 258 | + `created_at` INTEGER NULL DEFAULT (unixepoch()), 259 | + `updated_at` INTEGER NULL 260 | +); 261 | +CREATE UNIQUE INDEX `report_report_id_key` ON `report`(`report_id`); 262 | +CREATE INDEX `report_user_id_idx` ON `report`(`user_id`); 263 | +CREATE INDEX `report_website_id_idx` ON `report`(`website_id`); 264 | +CREATE INDEX `report_type_idx` ON `report`(`type`); 265 | +CREATE INDEX `report_name_idx` ON `report`(`name`); 266 | + 267 | +-- EventData migration 268 | +UPDATE event_data 269 | +SET string_value = number_value 270 | +WHERE data_type = 2; 271 | + 272 | +UPDATE event_data 273 | +SET string_value = strftime('%Y-%m-%dT%H:%M:%fZ', date_value, 'unixepoch') 274 | +WHERE data_type = 4; 275 | \ No newline at end of file 276 | diff --git a/db/sqlite/migrations/03_metric_performance_index/migration.sql b/db/sqlite/migrations/03_metric_performance_index/migration.sql 277 | new file mode 100644 278 | index 0000000..6468136 279 | --- /dev/null 280 | +++ b/db/sqlite/migrations/03_metric_performance_index/migration.sql 281 | @@ -0,0 +1,50 @@ 282 | +-- CreateIndex 283 | +CREATE INDEX `event_data_website_id_created_at_idx` ON `event_data`(`website_id`, `created_at`); 284 | + 285 | +-- CreateIndex 286 | +CREATE INDEX `event_data_website_id_created_at_event_key_idx` ON `event_data`(`website_id`, `created_at`, `event_key`); 287 | + 288 | +-- CreateIndex 289 | +CREATE INDEX `session_website_id_created_at_idx` ON `session`(`website_id`, `created_at`); 290 | + 291 | +-- CreateIndex 292 | +CREATE INDEX `session_website_id_created_at_hostname_idx` ON `session`(`website_id`, `created_at`, `hostname`); 293 | + 294 | +-- CreateIndex 295 | +CREATE INDEX `session_website_id_created_at_browser_idx` ON `session`(`website_id`, `created_at`, `browser`); 296 | + 297 | +-- CreateIndex 298 | +CREATE INDEX `session_website_id_created_at_os_idx` ON `session`(`website_id`, `created_at`, `os`); 299 | + 300 | +-- CreateIndex 301 | +CREATE INDEX `session_website_id_created_at_device_idx` ON `session`(`website_id`, `created_at`, `device`); 302 | + 303 | +-- CreateIndex 304 | +CREATE INDEX `session_website_id_created_at_screen_idx` ON `session`(`website_id`, `created_at`, `screen`); 305 | + 306 | +-- CreateIndex 307 | +CREATE INDEX `session_website_id_created_at_language_idx` ON `session`(`website_id`, `created_at`, `language`); 308 | + 309 | +-- CreateIndex 310 | +CREATE INDEX `session_website_id_created_at_country_idx` ON `session`(`website_id`, `created_at`, `country`); 311 | + 312 | +-- CreateIndex 313 | +CREATE INDEX `session_website_id_created_at_subdivision1_idx` ON `session`(`website_id`, `created_at`, `subdivision1`); 314 | + 315 | +-- CreateIndex 316 | +CREATE INDEX `session_website_id_created_at_city_idx` ON `session`(`website_id`, `created_at`, `city`); 317 | + 318 | +-- CreateIndex 319 | +CREATE INDEX `website_event_website_id_created_at_url_path_idx` ON `website_event`(`website_id`, `created_at`, `url_path`); 320 | + 321 | +-- CreateIndex 322 | +CREATE INDEX `website_event_website_id_created_at_url_query_idx` ON `website_event`(`website_id`, `created_at`, `url_query`); 323 | + 324 | +-- CreateIndex 325 | +CREATE INDEX `website_event_website_id_created_at_referrer_domain_idx` ON `website_event`(`website_id`, `created_at`, `referrer_domain`); 326 | + 327 | +-- CreateIndex 328 | +CREATE INDEX `website_event_website_id_created_at_page_title_idx` ON `website_event`(`website_id`, `created_at`, `page_title`); 329 | + 330 | +-- CreateIndex 331 | +CREATE INDEX `website_event_website_id_created_at_event_name_idx` ON `website_event`(`website_id`, `created_at`, `event_name`); 332 | diff --git a/db/sqlite/migrations/04_1_indexes_tables/migration.sql b/db/sqlite/migrations/04_1_indexes_tables/migration.sql 333 | new file mode 100644 334 | index 0000000..89418be 335 | --- /dev/null 336 | +++ b/db/sqlite/migrations/04_1_indexes_tables/migration.sql 337 | @@ -0,0 +1,206 @@ 338 | +-- Remove redundant indexes and clean table definitions 339 | +BEGIN TRANSACTION; 340 | +-- Create new tables 341 | +CREATE TABLE "new_user" ( 342 | + "user_id" TEXT NOT NULL, 343 | + "username" TEXT NOT NULL, 344 | + "password" TEXT NOT NULL, 345 | + "role" TEXT NOT NULL, 346 | + "created_at" INTEGER DEFAULT (unixepoch()), 347 | + "updated_at" INTEGER, 348 | + "deleted_at" INTEGER 349 | +); 350 | +CREATE TABLE "new_session" ( 351 | + "session_id" TEXT NOT NULL, 352 | + "website_id" TEXT NOT NULL, 353 | + "hostname" TEXT, 354 | + "browser" TEXT, 355 | + "os" TEXT, 356 | + "device" TEXT, 357 | + "screen" TEXT, 358 | + "language" TEXT, 359 | + "country" TEXT, 360 | + "subdivision1" TEXT, 361 | + "subdivision2" TEXT, 362 | + "city" TEXT, 363 | + "created_at" INTEGER DEFAULT (unixepoch()) 364 | +); 365 | +CREATE TABLE "new_website" ( 366 | + "website_id" TEXT NOT NULL, 367 | + "name" TEXT NOT NULL, 368 | + "domain" TEXT, 369 | + "share_id" TEXT, 370 | + "reset_at" INTEGER, 371 | + "user_id" TEXT, 372 | + "created_at" INTEGER DEFAULT (unixepoch()), 373 | + "updated_at" INTEGER, 374 | + "deleted_at" INTEGER 375 | +); 376 | +CREATE TABLE "new_website_event" ( 377 | + "event_id" TEXT NOT NULL, 378 | + "website_id" TEXT NOT NULL, 379 | + "session_id" TEXT NOT NULL, 380 | + "created_at" INTEGER DEFAULT (unixepoch()), 381 | + "url_path" TEXT NOT NULL, 382 | + "url_query" TEXT, 383 | + "referrer_path" TEXT, 384 | + "referrer_query" TEXT, 385 | + "referrer_domain" TEXT, 386 | + "page_title" TEXT, 387 | + "event_type" INTEGER NOT NULL DEFAULT 1, 388 | + "event_name" TEXT 389 | +); 390 | +CREATE TABLE "new_event_data" ( 391 | + "event_data_id" TEXT NOT NULL, 392 | + "website_event_id" TEXT NOT NULL, 393 | + "website_id" TEXT NOT NULL, 394 | + "event_key" TEXT NOT NULL, 395 | + "string_value" TEXT, 396 | + "number_value" NUMERIC, 397 | + "date_value" INTEGER, 398 | + "data_type" INTEGER NOT NULL, 399 | + "created_at" INTEGER DEFAULT (unixepoch()) 400 | +); 401 | +CREATE TABLE "new_team" ( 402 | + "team_id" TEXT NOT NULL, 403 | + "name" TEXT NOT NULL, 404 | + "access_code" TEXT, 405 | + "created_at" INTEGER DEFAULT (unixepoch()), 406 | + "updated_at" INTEGER 407 | +); 408 | +CREATE TABLE "new_team_user" ( 409 | + "team_user_id" TEXT NOT NULL, 410 | + "team_id" TEXT NOT NULL, 411 | + "user_id" TEXT NOT NULL, 412 | + "role" TEXT NOT NULL, 413 | + "created_at" INTEGER DEFAULT (unixepoch()), 414 | + "updated_at" INTEGER 415 | +); 416 | +CREATE TABLE "new_team_website" ( 417 | + "team_website_id" TEXT NOT NULL, 418 | + "team_id" TEXT NOT NULL, 419 | + "website_id" TEXT NOT NULL, 420 | + "created_at" INTEGER DEFAULT (unixepoch()) 421 | +); 422 | +CREATE TABLE "new_session_data" ( 423 | + "session_data_id" TEXT NOT NULL, 424 | + "website_id" TEXT NOT NULL, 425 | + "session_id" TEXT NOT NULL, 426 | + "event_key" TEXT NOT NULL, 427 | + "string_value" TEXT, 428 | + "number_value" NUMERIC, 429 | + "date_value" INTEGER, 430 | + "data_type" INTEGER NOT NULL, 431 | + "created_at" INTEGER DEFAULT (unixepoch()) 432 | +); 433 | +CREATE TABLE "new_report" ( 434 | + "report_id" TEXT NOT NULL, 435 | + "user_id" TEXT NOT NULL, 436 | + "website_id" TEXT NOT NULL, 437 | + "type" TEXT NOT NULL, 438 | + "name" TEXT NOT NULL, 439 | + "description" TEXT NOT NULL, 440 | + "parameters" TEXT NOT NULL, 441 | + "created_at" INTEGER DEFAULT (unixepoch()), 442 | + "updated_at" INTEGER 443 | +); 444 | +-- Transfer data 445 | +INSERT INTO "new_user" SELECT "user_id", "username", "password", "role", "created_at", "updated_at", "deleted_at" FROM "user"; 446 | +INSERT INTO "new_session" SELECT "session_id", "website_id", "hostname", "browser", "os", "device", "screen", "language", "country", "subdivision1", "subdivision2", "city", "created_at" FROM "session"; 447 | +INSERT INTO "new_website" SELECT "website_id", "name", "domain", "share_id", "reset_at", "user_id", "created_at", "updated_at", "deleted_at" FROM "website"; 448 | +INSERT INTO "new_website_event" SELECT "event_id", "website_id", "session_id", "created_at", "url_path", "url_query", "referrer_path", "referrer_query", "referrer_domain", "page_title", "event_type", "event_name" FROM "website_event"; 449 | +INSERT INTO "new_event_data" SELECT "event_data_id", "website_event_id", "website_id", "event_key", "string_value", "number_value", "date_value", "data_type", "created_at" FROM "event_data"; 450 | +INSERT INTO "new_team" SELECT "team_id", "name", "access_code", "created_at", "updated_at" FROM "team"; 451 | +INSERT INTO "new_team_user" SELECT "team_user_id", "team_id", "user_id", "role", "created_at", "updated_at" FROM "team_user"; 452 | +INSERT INTO "new_team_website" SELECT "team_website_id", "team_id", "website_id", "created_at" FROM "team_website"; 453 | +INSERT INTO "new_session_data" SELECT "session_data_id", "website_id", "session_id", "event_key", "string_value", "number_value", "date_value", "data_type", "created_at" FROM "session_data"; 454 | +INSERT INTO "new_report" SELECT "report_id", "user_id", "website_id", "type", "name", "description", "parameters", "created_at", "updated_at" FROM "report"; 455 | +-- Drop tables 456 | +DROP TABLE "user"; 457 | +DROP TABLE "session"; 458 | +DROP TABLE "website"; 459 | +DROP TABLE "website_event"; 460 | +DROP TABLE "event_data"; 461 | +DROP TABLE "team"; 462 | +DROP TABLE "team_user"; 463 | +DROP TABLE "team_website"; 464 | +DROP TABLE "session_data"; 465 | +DROP TABLE "report"; 466 | +-- Rename tables 467 | +ALTER TABLE "new_user" RENAME TO "user"; 468 | +ALTER TABLE "new_session" RENAME TO "session"; 469 | +ALTER TABLE "new_website" RENAME TO "website"; 470 | +ALTER TABLE "new_website_event" RENAME TO "website_event"; 471 | +ALTER TABLE "new_event_data" RENAME TO "event_data"; 472 | +ALTER TABLE "new_team" RENAME TO "team"; 473 | +ALTER TABLE "new_team_user" RENAME TO "team_user"; 474 | +ALTER TABLE "new_team_website" RENAME TO "team_website"; 475 | +ALTER TABLE "new_session_data" RENAME TO "session_data"; 476 | +ALTER TABLE "new_report" RENAME TO "report"; 477 | +-- Create indexes 478 | +CREATE UNIQUE INDEX "user_user_id_key" ON "user"("user_id"); 479 | +CREATE UNIQUE INDEX "user_username_key" ON "user"("username"); 480 | + 481 | +CREATE UNIQUE INDEX "session_session_id_key" ON "session"("session_id"); 482 | +CREATE INDEX "session_created_at_idx" ON "session"("created_at"); 483 | +CREATE INDEX "session_website_id_idx" ON "session"("website_id"); 484 | +CREATE INDEX "session_website_id_created_at_idx" ON "session"("website_id", "created_at"); 485 | +CREATE INDEX "session_website_id_created_at_hostname_idx" ON "session"("website_id", "created_at", "hostname"); 486 | +CREATE INDEX "session_website_id_created_at_browser_idx" ON "session"("website_id", "created_at", "browser"); 487 | +CREATE INDEX "session_website_id_created_at_os_idx" ON "session"("website_id", "created_at", "os"); 488 | +CREATE INDEX "session_website_id_created_at_device_idx" ON "session"("website_id", "created_at", "device"); 489 | +CREATE INDEX "session_website_id_created_at_screen_idx" ON "session"("website_id", "created_at", "screen"); 490 | +CREATE INDEX "session_website_id_created_at_language_idx" ON "session"("website_id", "created_at", "language"); 491 | +CREATE INDEX "session_website_id_created_at_country_idx" ON "session"("website_id", "created_at", "country"); 492 | +CREATE INDEX "session_website_id_created_at_subdivision1_idx" ON "session"("website_id", "created_at", "subdivision1"); 493 | +CREATE INDEX "session_website_id_created_at_city_idx" ON "session"("website_id", "created_at", "city"); 494 | + 495 | +CREATE UNIQUE INDEX "website_website_id_key" ON "website"("website_id"); 496 | +CREATE UNIQUE INDEX "website_share_id_key" ON "website"("share_id"); 497 | +CREATE INDEX "website_user_id_idx" ON "website"("user_id"); 498 | +CREATE INDEX "website_created_at_idx" ON "website"("created_at"); 499 | + 500 | +CREATE UNIQUE INDEX "website_event_event_id_key" ON "website_event"("event_id"); 501 | +CREATE INDEX "website_event_created_at_idx" ON "website_event"("created_at"); 502 | +CREATE INDEX "website_event_session_id_idx" ON "website_event"("session_id"); 503 | +CREATE INDEX "website_event_website_id_idx" ON "website_event"("website_id"); 504 | +CREATE INDEX "website_event_website_id_created_at_idx" ON "website_event"("website_id", "created_at"); 505 | +CREATE INDEX "website_event_website_id_session_id_created_at_idx" ON "website_event"("website_id", "session_id", "created_at"); 506 | +CREATE INDEX "website_event_website_id_created_at_url_path_idx" ON "website_event"("website_id", "created_at", "url_path"); 507 | +CREATE INDEX "website_event_website_id_created_at_url_query_idx" ON "website_event"("website_id", "created_at", "url_query"); 508 | +CREATE INDEX "website_event_website_id_created_at_referrer_domain_idx" ON "website_event"("website_id", "created_at", "referrer_domain"); 509 | +CREATE INDEX "website_event_website_id_created_at_page_title_idx" ON "website_event"("website_id", "created_at", "page_title"); 510 | +CREATE INDEX "website_event_website_id_created_at_event_name_idx" ON "website_event"("website_id", "created_at", "event_name"); 511 | + 512 | +CREATE UNIQUE INDEX "event_data_event_data_id_key" ON "event_data"("event_data_id"); 513 | +CREATE INDEX "event_data_created_at_idx" ON "event_data"("created_at"); 514 | +CREATE INDEX "event_data_website_id_idx" ON "event_data"("website_id"); 515 | +CREATE INDEX "event_data_website_event_id_idx" ON "event_data"("website_event_id"); 516 | +CREATE INDEX "event_data_website_id_website_event_id_created_at_idx" ON "event_data"("website_id", "website_event_id", "created_at"); 517 | +CREATE INDEX "event_data_website_id_created_at_idx" ON "event_data"("website_id", "created_at"); 518 | +CREATE INDEX "event_data_website_id_created_at_event_key_idx" ON "event_data"("website_id", "created_at", "event_key"); 519 | + 520 | +CREATE UNIQUE INDEX "team_team_id_key" ON "team"("team_id"); 521 | +CREATE UNIQUE INDEX "team_access_code_key" ON "team"("access_code"); 522 | + 523 | +CREATE UNIQUE INDEX "team_user_team_user_id_key" ON "team_user"("team_user_id"); 524 | +CREATE INDEX "team_user_team_id_idx" ON "team_user"("team_id"); 525 | +CREATE INDEX "team_user_user_id_idx" ON "team_user"("user_id"); 526 | + 527 | +CREATE UNIQUE INDEX "team_website_team_website_id_key" ON "team_website"("team_website_id"); 528 | +CREATE INDEX "team_website_team_id_idx" ON "team_website"("team_id"); 529 | +CREATE INDEX "team_website_website_id_idx" ON "team_website"("website_id"); 530 | + 531 | +CREATE UNIQUE INDEX "session_data_session_data_id_key" ON "session_data"("session_data_id"); 532 | +CREATE INDEX "session_data_created_at_idx" ON "session_data"("created_at"); 533 | +CREATE INDEX "session_data_website_id_idx" ON "session_data"("website_id"); 534 | +CREATE INDEX "session_data_session_id_idx" ON "session_data"("session_id"); 535 | + 536 | +CREATE UNIQUE INDEX "report_report_id_key" ON "report"("report_id"); 537 | +CREATE INDEX "report_user_id_idx" ON "report"("user_id"); 538 | +CREATE INDEX "report_website_id_idx" ON "report"("website_id"); 539 | +CREATE INDEX "report_type_idx" ON "report"("type"); 540 | +CREATE INDEX "report_name_idx" ON "report"("name"); 541 | +COMMIT; 542 | + 543 | +VACUUM; 544 | \ No newline at end of file 545 | diff --git a/db/sqlite/migrations/04_team_redesign/migration.sql b/db/sqlite/migrations/04_team_redesign/migration.sql 546 | new file mode 100644 547 | index 0000000..e2a6ed8 548 | --- /dev/null 549 | +++ b/db/sqlite/migrations/04_team_redesign/migration.sql 550 | @@ -0,0 +1,31 @@ 551 | +/* 552 | + Warnings: 553 | + 554 | + - You are about to drop the `team_website` table. If the table is not empty, all the data it contains will be lost. 555 | + 556 | +*/ 557 | +BEGIN TRANSACTION; 558 | +-- AlterTable 559 | +ALTER TABLE "team" ADD COLUMN "deleted_at" INTEGER; 560 | +ALTER TABLE "team" ADD COLUMN "logo_url" TEXT; 561 | + 562 | +-- AlterTable 563 | +ALTER TABLE "user" ADD COLUMN "display_name" TEXT; 564 | +ALTER TABLE "user" ADD COLUMN "logo_url" TEXT; 565 | + 566 | +-- AlterTable 567 | +ALTER TABLE "website" ADD COLUMN "created_by" TEXT; 568 | +ALTER TABLE "website" ADD COLUMN "team_id" TEXT; 569 | +COMMIT; 570 | + 571 | +-- MigrateData 572 | +UPDATE "website" SET created_by = user_id WHERE team_id IS NULL; 573 | + 574 | +-- DropTable 575 | +DROP TABLE "team_website"; 576 | + 577 | +-- CreateIndex 578 | +CREATE INDEX "website_team_id_idx" ON "website"("team_id"); 579 | + 580 | +-- CreateIndex 581 | +CREATE INDEX "website_created_by_idx" ON "website"("created_by"); 582 | \ No newline at end of file 583 | diff --git a/db/sqlite/migrations/05_add_visit_id/migration.sql b/db/sqlite/migrations/05_add_visit_id/migration.sql 584 | new file mode 100644 585 | index 0000000..0ccb534 586 | --- /dev/null 587 | +++ b/db/sqlite/migrations/05_add_visit_id/migration.sql 588 | @@ -0,0 +1,58 @@ 589 | +BEGIN TRANSACTION; 590 | + 591 | +CREATE TABLE "new_website_event" ( 592 | + "event_id" TEXT NOT NULL, 593 | + "website_id" TEXT NOT NULL, 594 | + "session_id" TEXT NOT NULL, 595 | + "visit_id" TEXT NOT NULL, 596 | + "created_at" INTEGER DEFAULT (unixepoch()), 597 | + "url_path" TEXT NOT NULL, 598 | + "url_query" TEXT, 599 | + "referrer_path" TEXT, 600 | + "referrer_query" TEXT, 601 | + "referrer_domain" TEXT, 602 | + "page_title" TEXT, 603 | + "event_type" INTEGER NOT NULL DEFAULT 1, 604 | + "event_name" TEXT 605 | +); 606 | + 607 | +INSERT INTO "new_website_event" SELECT "event_id", "website_id", "session_id", 'uuid' as "visit_id", "created_at", "url_path", "url_query", "referrer_path", "referrer_query", "referrer_domain", "page_title", "event_type", "event_name" FROM "website_event"; 608 | + 609 | +UPDATE "new_website_event" as we 610 | +SET visit_id = a.uuid 611 | +FROM (SELECT DISTINCT 612 | + s.session_id, 613 | + s.visit_time, 614 | + lower( 615 | + hex(randomblob(4)) 616 | + || '-' || hex(randomblob(2)) 617 | + || '-' || '4' || substr(hex(randomblob(2)), 2) 618 | + || '-' || substr('89AB', 1 + (abs(random()) % 4) , 1) || substr(hex(randomblob(2)), 2) 619 | + || '-' || hex(randomblob(6)) 620 | + ) uuid 621 | + FROM (SELECT DISTINCT session_id, 622 | + strftime('%Y-%m-%d %H:00:00', created_at, 'unixepoch') visit_time 623 | + FROM "website_event") s) a 624 | +WHERE we.session_id = a.session_id 625 | + and strftime('%Y-%m-%d %H:00:00', we.created_at, 'unixepoch') = a.visit_time; 626 | + 627 | +DROP TABLE "website_event"; 628 | + 629 | +ALTER TABLE "new_website_event" RENAME TO "website_event"; 630 | + 631 | +CREATE UNIQUE INDEX "website_event_event_id_key" ON "website_event"("event_id"); 632 | +CREATE INDEX "website_event_created_at_idx" ON "website_event"("created_at"); 633 | +CREATE INDEX "website_event_session_id_idx" ON "website_event"("session_id"); 634 | +CREATE INDEX "website_event_visit_id_idx" ON "website_event"("visit_id"); 635 | +CREATE INDEX "website_event_website_id_idx" ON "website_event"("website_id"); 636 | +CREATE INDEX "website_event_website_id_created_at_idx" ON "website_event"("website_id", "created_at"); 637 | +CREATE INDEX "website_event_website_id_session_id_created_at_idx" ON "website_event"("website_id", "session_id", "created_at"); 638 | +CREATE INDEX "website_event_website_id_created_at_url_path_idx" ON "website_event"("website_id", "created_at", "url_path"); 639 | +CREATE INDEX "website_event_website_id_created_at_url_query_idx" ON "website_event"("website_id", "created_at", "url_query"); 640 | +CREATE INDEX "website_event_website_id_created_at_referrer_domain_idx" ON "website_event"("website_id", "created_at", "referrer_domain"); 641 | +CREATE INDEX "website_event_website_id_created_at_page_title_idx" ON "website_event"("website_id", "created_at", "page_title"); 642 | +CREATE INDEX "website_event_website_id_created_at_event_name_idx" ON "website_event"("website_id", "created_at", "event_name"); 643 | +CREATE INDEX "website_event_website_id_visit_id_created_at_idx" ON "website_event"("website_id", "visit_id", "created_at"); 644 | + 645 | +COMMIT; 646 | +VACUUM; 647 | \ No newline at end of file 648 | diff --git a/db/sqlite/migrations/06_session_data/migration.sql b/db/sqlite/migrations/06_session_data/migration.sql 649 | new file mode 100644 650 | index 0000000..9ac346a 651 | --- /dev/null 652 | +++ b/db/sqlite/migrations/06_session_data/migration.sql 653 | @@ -0,0 +1,20 @@ 654 | +-- DropIndex 655 | +DROP INDEX "event_data_website_id_created_at_event_key_idx"; 656 | + 657 | +-- DropIndex 658 | +DROP INDEX "event_data_website_id_website_event_id_created_at_idx"; 659 | + 660 | +-- AlterTable 661 | +ALTER TABLE "event_data" RENAME COLUMN "event_key" TO "data_key"; 662 | + 663 | +-- AlterTable 664 | +ALTER TABLE "session_data" RENAME COLUMN "event_key" TO "data_key"; 665 | + 666 | +-- CreateIndex 667 | +CREATE INDEX "event_data_website_id_created_at_data_key_idx" ON "event_data"("website_id", "created_at", "data_key"); 668 | + 669 | +-- CreateIndex 670 | +CREATE INDEX "session_data_session_id_created_at_idx" ON "session_data"("session_id", "created_at"); 671 | + 672 | +-- CreateIndex 673 | +CREATE INDEX "session_data_website_id_created_at_data_key_idx" ON "session_data"("website_id", "created_at", "data_key"); 674 | diff --git a/db/sqlite/migrations/07_add_tag/migration.sql b/db/sqlite/migrations/07_add_tag/migration.sql 675 | new file mode 100644 676 | index 0000000..35ccefb 677 | --- /dev/null 678 | +++ b/db/sqlite/migrations/07_add_tag/migration.sql 679 | @@ -0,0 +1,5 @@ 680 | +-- AlterTable 681 | +ALTER TABLE "website_event" ADD COLUMN "tag" TEXT NULL; 682 | + 683 | +-- CreateIndex 684 | +CREATE INDEX "website_event_website_id_created_at_tag_idx" ON "website_event"("website_id", "created_at", "tag"); 685 | diff --git a/db/sqlite/migrations/08_1_datetime/migration.sql b/db/sqlite/migrations/08_1_datetime/migration.sql 686 | new file mode 100644 687 | index 0000000..2d0c031 688 | --- /dev/null 689 | +++ b/db/sqlite/migrations/08_1_datetime/migration.sql 690 | @@ -0,0 +1,145 @@ 691 | +BEGIN TRANSACTION; 692 | +PRAGMA writable_schema=ON; 693 | + 694 | +UPDATE sqlite_schema SET sql = 'CREATE TABLE "user" ( 695 | + "user_id" TEXT NOT NULL, 696 | + "username" TEXT NOT NULL, 697 | + "password" TEXT NOT NULL, 698 | + "role" TEXT NOT NULL, 699 | + "created_at" DATETIME DEFAULT (unixepoch() * 1000), 700 | + "updated_at" DATETIME, 701 | + "deleted_at" DATETIME 702 | +, "display_name" TEXT, "logo_url" TEXT)' WHERE type='table' AND name='user'; 703 | + 704 | +UPDATE sqlite_schema SET sql = 'CREATE TABLE "session" ( 705 | + "session_id" TEXT NOT NULL, 706 | + "website_id" TEXT NOT NULL, 707 | + "hostname" TEXT, 708 | + "browser" TEXT, 709 | + "os" TEXT, 710 | + "device" TEXT, 711 | + "screen" TEXT, 712 | + "language" TEXT, 713 | + "country" TEXT, 714 | + "subdivision1" TEXT, 715 | + "subdivision2" TEXT, 716 | + "city" TEXT, 717 | + "created_at" DATETIME DEFAULT (unixepoch() * 1000) 718 | +)' WHERE type='table' AND name='session'; 719 | + 720 | +UPDATE sqlite_schema SET sql = 'CREATE TABLE "website" ( 721 | + "website_id" TEXT NOT NULL, 722 | + "name" TEXT NOT NULL, 723 | + "domain" TEXT, 724 | + "share_id" TEXT, 725 | + "reset_at" DATETIME, 726 | + "user_id" TEXT, 727 | + "created_at" DATETIME DEFAULT (unixepoch() * 1000), 728 | + "updated_at" DATETIME, 729 | + "deleted_at" DATETIME 730 | +, "created_by" TEXT, "team_id" TEXT)' WHERE type='table' AND name='website'; 731 | + 732 | +UPDATE sqlite_schema SET sql = 'CREATE TABLE "event_data" ( 733 | + "event_data_id" TEXT NOT NULL, 734 | + "website_event_id" TEXT NOT NULL, 735 | + "website_id" TEXT NOT NULL, 736 | + "data_key" TEXT NOT NULL, 737 | + "string_value" TEXT, 738 | + "number_value" NUMERIC, 739 | + "date_value" DATETIME, 740 | + "data_type" INTEGER NOT NULL, 741 | + "created_at" DATETIME DEFAULT (unixepoch() * 1000) 742 | +)' WHERE type='table' AND name='event_data'; 743 | + 744 | +UPDATE sqlite_schema SET sql = 'CREATE TABLE "team" ( 745 | + "team_id" TEXT NOT NULL, 746 | + "name" TEXT NOT NULL, 747 | + "access_code" TEXT, 748 | + "created_at" DATETIME DEFAULT (unixepoch() * 1000), 749 | + "updated_at" DATETIME 750 | +, "deleted_at" DATETIME, "logo_url" TEXT)' WHERE type='table' AND name='team'; 751 | + 752 | +UPDATE sqlite_schema SET sql = 'CREATE TABLE "team_user" ( 753 | + "team_user_id" TEXT NOT NULL, 754 | + "team_id" TEXT NOT NULL, 755 | + "user_id" TEXT NOT NULL, 756 | + "role" TEXT NOT NULL, 757 | + "created_at" DATETIME DEFAULT (unixepoch() * 1000), 758 | + "updated_at" DATETIME 759 | +)' WHERE type='table' AND name='team_user'; 760 | + 761 | +UPDATE sqlite_schema SET sql = 'CREATE TABLE "session_data" ( 762 | + "session_data_id" TEXT NOT NULL, 763 | + "website_id" TEXT NOT NULL, 764 | + "session_id" TEXT NOT NULL, 765 | + "data_key" TEXT NOT NULL, 766 | + "string_value" TEXT, 767 | + "number_value" NUMERIC, 768 | + "date_value" DATETIME, 769 | + "data_type" INTEGER NOT NULL, 770 | + "created_at" DATETIME DEFAULT (unixepoch() * 1000) 771 | +)' WHERE type='table' AND name='session_data'; 772 | + 773 | +UPDATE sqlite_schema SET sql = 'CREATE TABLE "report" ( 774 | + "report_id" TEXT NOT NULL, 775 | + "user_id" TEXT NOT NULL, 776 | + "website_id" TEXT NOT NULL, 777 | + "type" TEXT NOT NULL, 778 | + "name" TEXT NOT NULL, 779 | + "description" TEXT NOT NULL, 780 | + "parameters" TEXT NOT NULL, 781 | + "created_at" DATETIME DEFAULT (unixepoch() * 1000), 782 | + "updated_at" DATETIME 783 | +)' WHERE type='table' AND name='report'; 784 | + 785 | +UPDATE sqlite_schema SET sql = 'CREATE TABLE "website_event" ( 786 | + "event_id" TEXT NOT NULL, 787 | + "website_id" TEXT NOT NULL, 788 | + "session_id" TEXT NOT NULL, 789 | + "visit_id" TEXT NOT NULL, 790 | + "created_at" DATETIME DEFAULT (unixepoch() * 1000), 791 | + "url_path" TEXT NOT NULL, 792 | + "url_query" TEXT, 793 | + "referrer_path" TEXT, 794 | + "referrer_query" TEXT, 795 | + "referrer_domain" TEXT, 796 | + "page_title" TEXT, 797 | + "event_type" INTEGER NOT NULL DEFAULT 1, 798 | + "event_name" TEXT 799 | +, "tag" TEXT NULL)' WHERE type='table' AND name='website_event'; 800 | + 801 | +CREATE TABLE update_schema_version ( version INTEGER ); 802 | +DROP TABLE update_schema_version; 803 | +PRAGMA writable_schema=OFF; 804 | +COMMIT; 805 | + 806 | +UPDATE user SET created_at = created_at * 1000; 807 | +UPDATE user SET updated_at = updated_at * 1000 WHERE updated_at IS NOT NULL; 808 | +UPDATE user SET deleted_at = deleted_at * 1000 WHERE deleted_at IS NOT NULL; 809 | + 810 | +UPDATE session SET created_at = created_at * 1000; 811 | + 812 | +UPDATE website SET created_at = created_at * 1000; 813 | +UPDATE website SET updated_at = updated_at * 1000 WHERE updated_at IS NOT NULL; 814 | +UPDATE website SET deleted_at = deleted_at * 1000 WHERE deleted_at IS NOT NULL; 815 | +UPDATE website SET reset_at = reset_at * 1000 WHERE reset_at IS NOT NULL; 816 | + 817 | +UPDATE event_data SET created_at = created_at * 1000; 818 | +UPDATE event_data SET date_value = date_value * 1000 WHERE date_value IS NOT NULL; 819 | + 820 | +UPDATE team SET created_at = created_at * 1000; 821 | +UPDATE team SET updated_at = updated_at * 1000 WHERE updated_at IS NOT NULL; 822 | +UPDATE team SET deleted_at = deleted_at * 1000 WHERE deleted_at IS NOT NULL; 823 | + 824 | +UPDATE team_user SET created_at = created_at * 1000; 825 | +UPDATE team_user SET updated_at = updated_at * 1000 WHERE updated_at IS NOT NULL; 826 | + 827 | +UPDATE session_data SET created_at = created_at * 1000; 828 | +UPDATE session_data SET date_value = date_value * 1000 WHERE date_value IS NOT NULL; 829 | + 830 | +UPDATE report SET created_at = created_at * 1000; 831 | +UPDATE report SET updated_at = updated_at * 1000 WHERE updated_at IS NOT NULL; 832 | + 833 | +UPDATE website_event SET created_at = created_at * 1000; 834 | + 835 | +VACUUM; 836 | \ No newline at end of file 837 | diff --git a/db/sqlite/migrations/08_add_utm_clid/migration.sql b/db/sqlite/migrations/08_add_utm_clid/migration.sql 838 | new file mode 100644 839 | index 0000000..afbd643 840 | --- /dev/null 841 | +++ b/db/sqlite/migrations/08_add_utm_clid/migration.sql 842 | @@ -0,0 +1,12 @@ 843 | +-- AlterTable 844 | +ALTER TABLE "website_event" ADD COLUMN "fbclid" TEXT; 845 | +ALTER TABLE "website_event" ADD COLUMN "gclid" TEXT; 846 | +ALTER TABLE "website_event" ADD COLUMN "li_fat_id" TEXT; 847 | +ALTER TABLE "website_event" ADD COLUMN "msclkid" TEXT; 848 | +ALTER TABLE "website_event" ADD COLUMN "ttclid" TEXT; 849 | +ALTER TABLE "website_event" ADD COLUMN "twclid" TEXT; 850 | +ALTER TABLE "website_event" ADD COLUMN "utm_campaign" TEXT; 851 | +ALTER TABLE "website_event" ADD COLUMN "utm_content" TEXT; 852 | +ALTER TABLE "website_event" ADD COLUMN "utm_medium" TEXT; 853 | +ALTER TABLE "website_event" ADD COLUMN "utm_source" TEXT; 854 | +ALTER TABLE "website_event" ADD COLUMN "utm_term" TEXT; 855 | diff --git a/db/sqlite/migrations/09_update_hostname_region/migration.sql b/db/sqlite/migrations/09_update_hostname_region/migration.sql 856 | new file mode 100644 857 | index 0000000..28e7ead 858 | --- /dev/null 859 | +++ b/db/sqlite/migrations/09_update_hostname_region/migration.sql 860 | @@ -0,0 +1,22 @@ 861 | +-- AlterTable 862 | +ALTER TABLE "website_event" ADD COLUMN "hostname" TEXT; 863 | + 864 | +-- DataMigration 865 | +UPDATE "website_event" AS w 866 | +SET hostname = s.hostname 867 | +FROM "session" AS s 868 | +WHERE s.website_id = w.website_id 869 | + and s.session_id = w.session_id; 870 | + 871 | +-- DropIndex 872 | +DROP INDEX "session_website_id_created_at_hostname_idx"; 873 | +DROP INDEX "session_website_id_created_at_subdivision1_idx"; 874 | + 875 | +-- AlterTable 876 | +ALTER TABLE "session" RENAME COLUMN "subdivision1" TO "region"; 877 | +ALTER TABLE "session" DROP COLUMN "subdivision2"; 878 | +ALTER TABLE "session" DROP COLUMN "hostname"; 879 | + 880 | +-- CreateIndex 881 | +CREATE INDEX "website_event_website_id_created_at_hostname_idx" ON "website_event"("website_id", "created_at", "hostname"); 882 | +CREATE INDEX "session_website_id_created_at_region_idx" ON "session"("website_id", "created_at", "region"); 883 | diff --git a/db/sqlite/migrations/10_add_distinct_id/migration.sql b/db/sqlite/migrations/10_add_distinct_id/migration.sql 884 | new file mode 100644 885 | index 0000000..c25de1b 886 | --- /dev/null 887 | +++ b/db/sqlite/migrations/10_add_distinct_id/migration.sql 888 | @@ -0,0 +1,5 @@ 889 | +-- AlterTable 890 | +ALTER TABLE "session" ADD COLUMN "distinct_id" TEXT; 891 | + 892 | +-- AlterTable 893 | +ALTER TABLE "session_data" ADD COLUMN "distinct_id" TEXT; 894 | diff --git a/db/sqlite/migrations/migration_lock.toml b/db/sqlite/migrations/migration_lock.toml 895 | new file mode 100644 896 | index 0000000..e5e5c47 897 | --- /dev/null 898 | +++ b/db/sqlite/migrations/migration_lock.toml 899 | @@ -0,0 +1,3 @@ 900 | +# Please do not edit this file manually 901 | +# It should be added in your version-control system (i.e. Git) 902 | +provider = "sqlite" 903 | \ No newline at end of file 904 | diff --git a/db/sqlite/schema.prisma b/db/sqlite/schema.prisma 905 | new file mode 100644 906 | index 0000000..f031298 907 | --- /dev/null 908 | +++ b/db/sqlite/schema.prisma 909 | @@ -0,0 +1,231 @@ 910 | +generator client { 911 | + provider = "prisma-client-js" 912 | +} 913 | + 914 | +datasource db { 915 | + provider = "sqlite" 916 | + url = env("DATABASE_URL") 917 | + relationMode = "prisma" 918 | +} 919 | + 920 | +model User { 921 | + id String @unique @map("user_id") 922 | + username String @unique 923 | + password String 924 | + role String @map("role") 925 | + logoUrl String? @map("logo_url") 926 | + displayName String? @map("display_name") 927 | + createdAt DateTime? @default(dbgenerated("unixepoch() * 1000")) @map("created_at") 928 | + updatedAt DateTime? @map("updated_at") 929 | + deletedAt DateTime? @map("deleted_at") 930 | + 931 | + websiteUser Website[] @relation("user") 932 | + websiteCreateUser Website[] @relation("createUser") 933 | + teamUser TeamUser[] 934 | + report Report[] 935 | + 936 | + @@map("user") 937 | +} 938 | + 939 | +model Session { 940 | + id String @unique @map("session_id") 941 | + websiteId String @map("website_id") 942 | + browser String? 943 | + os String? 944 | + device String? 945 | + screen String? 946 | + language String? 947 | + country String? 948 | + region String? 949 | + city String? 950 | + distinctId String? @map("distinct_id") 951 | + createdAt DateTime? @default(dbgenerated("unixepoch() * 1000")) @map("created_at") 952 | + 953 | + websiteEvent WebsiteEvent[] 954 | + sessionData SessionData[] 955 | + 956 | + @@index([createdAt]) 957 | + @@index([websiteId]) 958 | + @@index([websiteId, createdAt]) 959 | + @@index([websiteId, createdAt, browser]) 960 | + @@index([websiteId, createdAt, os]) 961 | + @@index([websiteId, createdAt, device]) 962 | + @@index([websiteId, createdAt, screen]) 963 | + @@index([websiteId, createdAt, language]) 964 | + @@index([websiteId, createdAt, country]) 965 | + @@index([websiteId, createdAt, region]) 966 | + @@index([websiteId, createdAt, city]) 967 | + @@map("session") 968 | +} 969 | + 970 | +model Website { 971 | + id String @unique @map("website_id") 972 | + name String 973 | + domain String? 974 | + shareId String? @unique @map("share_id") 975 | + resetAt DateTime? @map("reset_at") 976 | + userId String? @map("user_id") 977 | + teamId String? @map("team_id") 978 | + createdBy String? @map("created_by") 979 | + createdAt DateTime? @default(dbgenerated("unixepoch() * 1000")) @map("created_at") 980 | + updatedAt DateTime? @map("updated_at") 981 | + deletedAt DateTime? @map("deleted_at") 982 | + 983 | + user User? @relation("user", fields: [userId], references: [id]) 984 | + createUser User? @relation("createUser", fields: [createdBy], references: [id]) 985 | + team Team? @relation(fields: [teamId], references: [id]) 986 | + eventData EventData[] 987 | + report Report[] 988 | + sessionData SessionData[] 989 | + 990 | + @@index([userId]) 991 | + @@index([teamId]) 992 | + @@index([createdAt]) 993 | + @@index([createdBy]) 994 | + @@map("website") 995 | +} 996 | + 997 | +model WebsiteEvent { 998 | + id String @unique @map("event_id") 999 | + websiteId String @map("website_id") 1000 | + sessionId String @map("session_id") 1001 | + visitId String @map("visit_id") 1002 | + createdAt DateTime? @default(dbgenerated("unixepoch() * 1000")) @map("created_at") 1003 | + urlPath String @map("url_path") 1004 | + urlQuery String? @map("url_query") 1005 | + utmSource String? @map("utm_source") 1006 | + utmMedium String? @map("utm_medium") 1007 | + utmCampaign String? @map("utm_campaign") 1008 | + utmContent String? @map("utm_content") 1009 | + utmTerm String? @map("utm_term") 1010 | + referrerPath String? @map("referrer_path") 1011 | + referrerQuery String? @map("referrer_query") 1012 | + referrerDomain String? @map("referrer_domain") 1013 | + pageTitle String? @map("page_title") 1014 | + gclid String? @map("gclid") 1015 | + fbclid String? @map("fbclid") 1016 | + msclkid String? @map("msclkid") 1017 | + ttclid String? @map("ttclid") 1018 | + lifatid String? @map("li_fat_id") 1019 | + twclid String? @map("twclid") 1020 | + eventType Int @default(1) @map("event_type") 1021 | + eventName String? @map("event_name") 1022 | + tag String? 1023 | + hostname String? 1024 | + 1025 | + eventData EventData[] 1026 | + session Session @relation(fields: [sessionId], references: [id]) 1027 | + 1028 | + @@index([createdAt]) 1029 | + @@index([sessionId]) 1030 | + @@index([visitId]) 1031 | + @@index([websiteId]) 1032 | + @@index([websiteId, createdAt]) 1033 | + @@index([websiteId, createdAt, urlPath]) 1034 | + @@index([websiteId, createdAt, urlQuery]) 1035 | + @@index([websiteId, createdAt, referrerDomain]) 1036 | + @@index([websiteId, createdAt, pageTitle]) 1037 | + @@index([websiteId, createdAt, eventName]) 1038 | + @@index([websiteId, createdAt, tag]) 1039 | + @@index([websiteId, sessionId, createdAt]) 1040 | + @@index([websiteId, visitId, createdAt]) 1041 | + @@index([websiteId, createdAt, hostname]) 1042 | + @@map("website_event") 1043 | +} 1044 | + 1045 | +model EventData { 1046 | + id String @unique @map("event_data_id") 1047 | + websiteId String @map("website_id") 1048 | + websiteEventId String @map("website_event_id") 1049 | + dataKey String @map("data_key") 1050 | + stringValue String? @map("string_value") 1051 | + numberValue Decimal? @map("number_value") 1052 | + dateValue DateTime? @map("date_value") 1053 | + dataType Int @map("data_type") 1054 | + createdAt DateTime? @default(dbgenerated("unixepoch() * 1000")) @map("created_at") 1055 | + 1056 | + website Website @relation(fields: [websiteId], references: [id]) 1057 | + websiteEvent WebsiteEvent @relation(fields: [websiteEventId], references: [id]) 1058 | + 1059 | + @@index([createdAt]) 1060 | + @@index([websiteId]) 1061 | + @@index([websiteEventId]) 1062 | + @@index([websiteId, createdAt]) 1063 | + @@index([websiteId, createdAt, dataKey]) 1064 | + @@map("event_data") 1065 | +} 1066 | + 1067 | +model SessionData { 1068 | + id String @unique @map("session_data_id") 1069 | + websiteId String @map("website_id") 1070 | + sessionId String @map("session_id") 1071 | + dataKey String @map("data_key") 1072 | + stringValue String? @map("string_value") 1073 | + numberValue Decimal? @map("number_value") 1074 | + dateValue DateTime? @map("date_value") 1075 | + dataType Int @map("data_type") 1076 | + distinctId String? @map("distinct_id") 1077 | + createdAt DateTime? @default(dbgenerated("unixepoch() * 1000")) @map("created_at") 1078 | + 1079 | + website Website @relation(fields: [websiteId], references: [id]) 1080 | + session Session @relation(fields: [sessionId], references: [id]) 1081 | + 1082 | + @@index([createdAt]) 1083 | + @@index([websiteId]) 1084 | + @@index([sessionId]) 1085 | + @@index([sessionId, createdAt]) 1086 | + @@index([websiteId, createdAt, dataKey]) 1087 | + @@map("session_data") 1088 | +} 1089 | + 1090 | +model Team { 1091 | + id String @unique @map("team_id") 1092 | + name String 1093 | + accessCode String? @unique @map("access_code") 1094 | + logoUrl String? @map("logo_url") 1095 | + createdAt DateTime? @default(dbgenerated("unixepoch() * 1000")) @map("created_at") 1096 | + updatedAt DateTime? @map("updated_at") 1097 | + deletedAt DateTime? @map("deleted_at") 1098 | + 1099 | + website Website[] 1100 | + teamUser TeamUser[] 1101 | + 1102 | + @@map("team") 1103 | +} 1104 | + 1105 | +model TeamUser { 1106 | + id String @unique @map("team_user_id") 1107 | + teamId String @map("team_id") 1108 | + userId String @map("user_id") 1109 | + role String @map("role") 1110 | + createdAt DateTime? @default(dbgenerated("unixepoch() * 1000")) @map("created_at") 1111 | + updatedAt DateTime? @map("updated_at") 1112 | + 1113 | + team Team @relation(fields: [teamId], references: [id]) 1114 | + user User @relation(fields: [userId], references: [id]) 1115 | + 1116 | + @@index([teamId]) 1117 | + @@index([userId]) 1118 | + @@map("team_user") 1119 | +} 1120 | + 1121 | +model Report { 1122 | + id String @unique @map("report_id") 1123 | + userId String @map("user_id") 1124 | + websiteId String @map("website_id") 1125 | + type String @map("type") 1126 | + name String @map("name") 1127 | + description String @map("description") 1128 | + parameters String @map("parameters") 1129 | + createdAt DateTime? @default(dbgenerated("unixepoch() * 1000")) @map("created_at") 1130 | + updatedAt DateTime? @map("updated_at") 1131 | + 1132 | + user User @relation(fields: [userId], references: [id]) 1133 | + website Website @relation(fields: [websiteId], references: [id]) 1134 | + 1135 | + @@index([userId]) 1136 | + @@index([websiteId]) 1137 | + @@index([type]) 1138 | + @@index([name]) 1139 | + @@map("report") 1140 | +} 1141 | diff --git a/scripts/check-db.js b/scripts/check-db.js 1142 | index ca0fca3..a0ab55c 100644 1143 | --- a/scripts/check-db.js 1144 | +++ b/scripts/check-db.js 1145 | @@ -17,10 +17,15 @@ function getDatabaseType(url = process.env.DATABASE_URL) { 1146 | return 'postgresql'; 1147 | } 1148 | 1149 | + if (type === 'file') { 1150 | + return 'sqlite'; 1151 | + } 1152 | + 1153 | return type; 1154 | } 1155 | 1156 | const prisma = new PrismaClient(); 1157 | +const databaseType = getDatabaseType(); 1158 | 1159 | function success(msg) { 1160 | console.log(chalk.greenBright(`✓ ${msg}`)); 1161 | @@ -49,30 +54,32 @@ async function checkConnection() { 1162 | } 1163 | 1164 | async function checkDatabaseVersion() { 1165 | - const query = await prisma.$queryRaw`select version() as version`; 1166 | - const version = semver.valid(semver.coerce(query[0].version)); 1167 | + if (databaseType !== 'sqlite') { 1168 | + const query = await prisma.$queryRaw`select version() as version`; 1169 | + const version = semver.valid(semver.coerce(query[0].version)); 1170 | 1171 | - const databaseType = getDatabaseType(); 1172 | - const minVersion = databaseType === 'postgresql' ? '9.4.0' : '5.7.0'; 1173 | + const minVersion = databaseType === 'postgresql' ? '9.4.0' : '5.7.0'; 1174 | 1175 | - if (semver.lt(version, minVersion)) { 1176 | - throw new Error( 1177 | - `Database version is not compatible. Please upgrade ${databaseType} version to ${minVersion} or greater`, 1178 | - ); 1179 | - } 1180 | + if (semver.lt(version, minVersion)) { 1181 | + throw new Error( 1182 | + `Database version is not compatible. Please upgrade ${databaseType} version to ${minVersion} or greater`, 1183 | + ); 1184 | + } 1185 | 1186 | - success('Database version check successful.'); 1187 | + success('Database version check successful.'); 1188 | + } 1189 | } 1190 | 1191 | async function checkV1Tables() { 1192 | + // check for v1 migrations before v2 release date 1193 | + const releaseDate = (databaseType !== 'sqlite') ? "'2023-04-17'" : 1686268800000; 1194 | try { 1195 | - // check for v1 migrations before v2 release date 1196 | const record = 1197 | - await prisma.$queryRaw`select * from _prisma_migrations where started_at < '2023-04-17'`; 1198 | + await prisma.$queryRaw`select * from _prisma_migrations where started_at < ${releaseDate}`; 1199 | 1200 | if (record.length > 0) { 1201 | error( 1202 | - 'Umami v1 tables detected. For how to upgrade from v1 to v2 go to https://umami.is/docs/migrate-v1-v2.', 1203 | + 'Umami v1 tables detected.', 1204 | ); 1205 | process.exit(1); 1206 | } 1207 | diff --git a/scripts/copy-db-files.js b/scripts/copy-db-files.js 1208 | index 15c3467..673db6b 100644 1209 | --- a/scripts/copy-db-files.js 1210 | +++ b/scripts/copy-db-files.js 1211 | @@ -11,12 +11,16 @@ function getDatabaseType(url = process.env.DATABASE_URL) { 1212 | return 'postgresql'; 1213 | } 1214 | 1215 | + if (type === 'file') { 1216 | + return 'sqlite'; 1217 | + } 1218 | + 1219 | return type; 1220 | } 1221 | 1222 | const databaseType = getDatabaseType(); 1223 | 1224 | -if (!databaseType || !['mysql', 'postgresql'].includes(databaseType)) { 1225 | +if (!databaseType || !['mysql', 'postgresql', 'sqlite'].includes(databaseType)) { 1226 | throw new Error('Missing or invalid database'); 1227 | } 1228 | 1229 | diff --git a/scripts/sqlite/convert-utm-clid-columns.js b/scripts/sqlite/convert-utm-clid-columns.js 1230 | new file mode 100644 1231 | index 0000000..82a7ba4 1232 | --- /dev/null 1233 | +++ b/scripts/sqlite/convert-utm-clid-columns.js 1234 | @@ -0,0 +1,62 @@ 1235 | +/* 1236 | +* < v2.18 data conversion 1237 | +*/ 1238 | + 1239 | +require('dotenv').config(); 1240 | +const { PrismaClient } = require('@prisma/client'); 1241 | + 1242 | +const UTM_CLID_LENGTH = 255; 1243 | + 1244 | +const columns = [ 1245 | + 'fbclid', 1246 | + 'gclid', 1247 | + 'li_fat_id', 1248 | + 'msclkid', 1249 | + 'ttclid', 1250 | + 'twclid', 1251 | + 'utm_campaign', 1252 | + 'utm_content', 1253 | + 'utm_medium', 1254 | + 'utm_source', 1255 | + 'utm_term', 1256 | +]; 1257 | + 1258 | +const regexes = columns.reduce((acc, column) => { 1259 | + acc[column] = new RegExp(`(?:[&?]|^)${column}=([^&]+)`, 'i'); 1260 | + return acc; 1261 | +}, {}); 1262 | + 1263 | +const prisma = new PrismaClient(); 1264 | + 1265 | +(async () => { 1266 | + try { 1267 | + const queries = []; 1268 | + const websiteEvents = await prisma.$queryRaw`SELECT event_id, url_query FROM website_event WHERE url_query <> ''`; 1269 | + 1270 | + websiteEvents.forEach(({ event_id, url_query }) => { 1271 | + const updates = []; 1272 | + 1273 | + for (const column of columns) { 1274 | + const match = url_query.match(regexes[column]); 1275 | + 1276 | + if (match) { 1277 | + updates.push(`${column} = '${match[1].substring(0, UTM_CLID_LENGTH)}'`); 1278 | + } 1279 | + } 1280 | + 1281 | + if (updates.length != 0) { 1282 | + queries.push(prisma.$executeRawUnsafe( 1283 | + `UPDATE website_event SET ${updates.join(', ')} WHERE event_id = '${event_id}'` 1284 | + )); 1285 | + } 1286 | + }); 1287 | + 1288 | + await prisma.$transaction(queries); 1289 | + await prisma.$disconnect(); 1290 | + console.log('Conversion completed.'); 1291 | + } catch (e) { 1292 | + console.error(e); 1293 | + await prisma.$disconnect(); 1294 | + process.exit(1); 1295 | + } 1296 | +})(); 1297 | \ No newline at end of file 1298 | diff --git a/scripts/sqlite/vacuum.js b/scripts/sqlite/vacuum.js 1299 | new file mode 100644 1300 | index 0000000..9f55049 1301 | --- /dev/null 1302 | +++ b/scripts/sqlite/vacuum.js 1303 | @@ -0,0 +1,10 @@ 1304 | +require('dotenv').config(); 1305 | +const { PrismaClient } = require('@prisma/client'); 1306 | + 1307 | +const prisma = new PrismaClient(); 1308 | + 1309 | +(async () => { 1310 | + await prisma.$connect(); 1311 | + await prisma.$executeRaw`VACUUM`; 1312 | + prisma.$disconnect(); 1313 | +})(); 1314 | diff --git a/src/lib/db.ts b/src/lib/db.ts 1315 | index 0ffedd0..856ab92 100644 1316 | --- a/src/lib/db.ts 1317 | +++ b/src/lib/db.ts 1318 | @@ -1,6 +1,7 @@ 1319 | export const PRISMA = 'prisma'; 1320 | export const POSTGRESQL = 'postgresql'; 1321 | export const MYSQL = 'mysql'; 1322 | +export const SQLITE = 'sqlite'; 1323 | export const CLICKHOUSE = 'clickhouse'; 1324 | export const KAFKA = 'kafka'; 1325 | export const KAFKA_PRODUCER = 'kafka-producer'; 1326 | @@ -13,6 +14,10 @@ BigInt.prototype['toJSON'] = function () { 1327 | export function getDatabaseType(url = process.env.DATABASE_URL) { 1328 | const type = url && url.split(':')[0]; 1329 | 1330 | + if (type === 'file') { 1331 | + return SQLITE; 1332 | + } 1333 | + 1334 | if (type === 'postgres') { 1335 | return POSTGRESQL; 1336 | } 1337 | @@ -21,6 +26,12 @@ export function getDatabaseType(url = process.env.DATABASE_URL) { 1338 | } 1339 | 1340 | export async function runQuery(queries: any) { 1341 | + const db = getDatabaseType(); 1342 | + 1343 | + if (db === SQLITE || db === POSTGRESQL || db === MYSQL) { 1344 | + return queries[PRISMA](); 1345 | + } 1346 | + 1347 | if (process.env.CLICKHOUSE_URL) { 1348 | if (queries[KAFKA]) { 1349 | return queries[KAFKA](); 1350 | @@ -28,12 +39,6 @@ export async function runQuery(queries: any) { 1351 | 1352 | return queries[CLICKHOUSE](); 1353 | } 1354 | - 1355 | - const db = getDatabaseType(); 1356 | - 1357 | - if (db === POSTGRESQL || db === MYSQL) { 1358 | - return queries[PRISMA](); 1359 | - } 1360 | } 1361 | 1362 | export function notImplemented() { 1363 | diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts 1364 | index b611123..2c5b740 100644 1365 | --- a/src/lib/prisma.ts 1366 | +++ b/src/lib/prisma.ts 1367 | @@ -2,7 +2,7 @@ import debug from 'debug'; 1368 | import { PrismaClient } from '@prisma/client'; 1369 | import { readReplicas } from '@prisma/extension-read-replicas'; 1370 | import { formatInTimeZone } from 'date-fns-tz'; 1371 | -import { MYSQL, POSTGRESQL, getDatabaseType } from '@/lib/db'; 1372 | +import { MYSQL, POSTGRESQL, SQLITE, getDatabaseType } from '@/lib/db'; 1373 | import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants'; 1374 | import { fetchWebsite } from './load'; 1375 | import { maxDate } from './date'; 1376 | @@ -37,9 +37,21 @@ const POSTGRESQL_DATE_FORMATS = { 1377 | year: 'YYYY-01-01 HH24:00:00', 1378 | }; 1379 | 1380 | +const SQLITE_DATE_FORMATS = { 1381 | + minute: '%Y-%m-%d %H:%M:00', 1382 | + hour: '%Y-%m-%d %H:00:00', 1383 | + day: '%Y-%m-%d 00:00:00', 1384 | + month: '%Y-%m-01 00:00:00', 1385 | + year: '%Y-01-01 00:00:00', 1386 | +}; 1387 | + 1388 | function getAddIntervalQuery(field: string, interval: string): string { 1389 | const db = getDatabaseType(); 1390 | 1391 | + if (db === SQLITE) { 1392 | + return `strftime('%s', ${field} / 1000, 'unixepoch', '${interval}')`; 1393 | + } 1394 | + 1395 | if (db === POSTGRESQL) { 1396 | return `${field} + interval '${interval}'`; 1397 | } 1398 | @@ -52,6 +64,10 @@ function getAddIntervalQuery(field: string, interval: string): string { 1399 | function getDayDiffQuery(field1: string, field2: string): string { 1400 | const db = getDatabaseType(); 1401 | 1402 | + if (db === SQLITE) { 1403 | + return `(unixepoch(${field1}) - unixepoch(${field2})) / 86400`; 1404 | + } 1405 | + 1406 | if (db === POSTGRESQL) { 1407 | return `${field1}::date - ${field2}::date`; 1408 | } 1409 | @@ -64,6 +80,10 @@ function getDayDiffQuery(field1: string, field2: string): string { 1410 | function getCastColumnQuery(field: string, type: string): string { 1411 | const db = getDatabaseType(); 1412 | 1413 | + if (db === SQLITE) { 1414 | + return `CAST(${field} as ${type})`; 1415 | + } 1416 | + 1417 | if (db === POSTGRESQL) { 1418 | return `${field}::${type}`; 1419 | } 1420 | @@ -73,9 +93,27 @@ function getCastColumnQuery(field: string, type: string): string { 1421 | } 1422 | } 1423 | 1424 | +function getISODateSQL(field: string): string { 1425 | + const db = getDatabaseType(); 1426 | + 1427 | + if (db === SQLITE) { 1428 | + return `strftime('%Y-%m-%dT%H:%M:%SZ', ${field} /1000, 'unixepoch')`; 1429 | + } else { 1430 | + return field; 1431 | + } 1432 | +} 1433 | + 1434 | function getDateSQL(field: string, unit: string, timezone?: string): string { 1435 | const db = getDatabaseType(); 1436 | 1437 | + if (db === SQLITE) { 1438 | + if(timezone) { 1439 | + const tz = formatInTimeZone(new Date(), timezone, 'xxx').substring(0, 3); 1440 | + return `strftime('${SQLITE_DATE_FORMATS[unit]}', ${field} / 1000, 'unixepoch', '${tz} hours')`; 1441 | + } 1442 | + return `strftime('${SQLITE_DATE_FORMATS[unit]}', ${field} /1000, 'unixepoch')`; 1443 | + } 1444 | + 1445 | if (db === POSTGRESQL) { 1446 | if (timezone) { 1447 | return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${POSTGRESQL_DATE_FORMATS[unit]}')`; 1448 | @@ -95,6 +133,11 @@ function getDateSQL(field: string, unit: string, timezone?: string): string { 1449 | function getDateWeeklySQL(field: string, timezone?: string) { 1450 | const db = getDatabaseType(); 1451 | 1452 | + if (db === SQLITE) { 1453 | + const tz = formatInTimeZone(new Date(), timezone, 'xxx').substring(0, 3); 1454 | + return `strftime('%w:%H', ${field} /1000, 'unixepoch', '${tz} hours')`; 1455 | + } 1456 | + 1457 | if (db === POSTGRESQL) { 1458 | return `concat(extract(dow from (${field} at time zone '${timezone}')), ':', to_char((${field} at time zone '${timezone}'), 'HH24'))`; 1459 | } 1460 | @@ -108,6 +151,10 @@ function getDateWeeklySQL(field: string, timezone?: string) { 1461 | export function getTimestampSQL(field: string) { 1462 | const db = getDatabaseType(); 1463 | 1464 | + if (db === SQLITE) { 1465 | + return `${field} / 1000`; 1466 | + } 1467 | + 1468 | if (db === POSTGRESQL) { 1469 | return `floor(extract(epoch from ${field}))`; 1470 | } 1471 | @@ -120,6 +167,10 @@ export function getTimestampSQL(field: string) { 1472 | function getTimestampDiffSQL(field1: string, field2: string): string { 1473 | const db = getDatabaseType(); 1474 | 1475 | + if (db === SQLITE) { 1476 | + return `(${field2} - ${field1}) / 1000`; 1477 | + } 1478 | + 1479 | if (db === POSTGRESQL) { 1480 | return `floor(extract(epoch from (${field2} - ${field1})))`; 1481 | } 1482 | @@ -231,7 +282,7 @@ async function rawQuery(sql: string, data: object): Promise { 1483 | const db = getDatabaseType(); 1484 | const params = []; 1485 | 1486 | - if (db !== POSTGRESQL && db !== MYSQL) { 1487 | + if (db !== SQLITE && db !== POSTGRESQL && db !== MYSQL) { 1488 | return Promise.reject(new Error('Unknown database.')); 1489 | } 1490 | 1491 | @@ -242,7 +293,7 @@ async function rawQuery(sql: string, data: object): Promise { 1492 | 1493 | params.push(value); 1494 | 1495 | - return db === MYSQL ? '?' : `$${params.length}${type ?? ''}`; 1496 | + return db !== POSTGRESQL ? '?' : `$${params.length}${type ?? ''}`; 1497 | }); 1498 | 1499 | return process.env.DATABASE_REPLICA_URL 1500 | @@ -391,6 +442,7 @@ export default { 1501 | getDateSQL, 1502 | getDateWeeklySQL, 1503 | getFilterQuery, 1504 | + getISODateSQL, 1505 | getSearchParameters, 1506 | getTimestampDiffSQL, 1507 | getSearchSQL, 1508 | diff --git a/src/queries/prisma/team.ts b/src/queries/prisma/team.ts 1509 | index 9862fff..08b6a49 100644 1510 | --- a/src/queries/prisma/team.ts 1511 | +++ b/src/queries/prisma/team.ts 1512 | @@ -106,10 +106,7 @@ export async function updateTeam(teamId: string, data: Prisma.TeamUpdateInput): 1513 | where: { 1514 | id: teamId, 1515 | }, 1516 | - data: { 1517 | - ...data, 1518 | - updatedAt: new Date(), 1519 | - }, 1520 | + data, 1521 | }); 1522 | } 1523 | 1524 | diff --git a/src/queries/sql/events/getWebsiteEvents.ts b/src/queries/sql/events/getWebsiteEvents.ts 1525 | index d47b066..09c89d6 100644 1526 | --- a/src/queries/sql/events/getWebsiteEvents.ts 1527 | +++ b/src/queries/sql/events/getWebsiteEvents.ts 1528 | @@ -13,7 +13,7 @@ export function getWebsiteEvents( 1529 | } 1530 | 1531 | async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) { 1532 | - const { pagedRawQuery, parseFilters } = prisma; 1533 | + const { pagedRawQuery, parseFilters, getISODateSQL } = prisma; 1534 | const { search } = pageParams; 1535 | const { filterQuery, params } = await parseFilters(websiteId, { 1536 | ...filters, 1537 | @@ -29,7 +29,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar 1538 | event_id as "id", 1539 | website_id as "websiteId", 1540 | session_id as "sessionId", 1541 | - created_at as "createdAt", 1542 | + ${getISODateSQL('created_at')} as "createdAt", 1543 | url_path as "urlPath", 1544 | url_query as "urlQuery", 1545 | referrer_path as "referrerPath", 1546 | diff --git a/src/queries/sql/getRealtimeActivity.ts b/src/queries/sql/getRealtimeActivity.ts 1547 | index 5f6646f..459dc0d 100644 1548 | --- a/src/queries/sql/getRealtimeActivity.ts 1549 | +++ b/src/queries/sql/getRealtimeActivity.ts 1550 | @@ -11,7 +11,7 @@ export async function getRealtimeActivity(...args: [websiteId: string, filters: 1551 | } 1552 | 1553 | async function relationalQuery(websiteId: string, filters: QueryFilters) { 1554 | - const { rawQuery, parseFilters } = prisma; 1555 | + const { rawQuery, parseFilters, getISODateSQL } = prisma; 1556 | const { params, filterQuery, dateQuery } = await parseFilters(websiteId, filters); 1557 | 1558 | return rawQuery( 1559 | @@ -19,7 +19,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { 1560 | select 1561 | website_event.session_id as "sessionId", 1562 | website_event.event_name as "eventName", 1563 | - website_event.created_at as "createdAt", 1564 | + ${getISODateSQL('website_event.created_at')} as "createdAt", 1565 | session.browser, 1566 | session.os, 1567 | session.device, 1568 | diff --git a/src/queries/sql/getWebsiteDateRange.ts b/src/queries/sql/getWebsiteDateRange.ts 1569 | index 953fa5e..fb92465 100644 1570 | --- a/src/queries/sql/getWebsiteDateRange.ts 1571 | +++ b/src/queries/sql/getWebsiteDateRange.ts 1572 | @@ -11,14 +11,14 @@ export async function getWebsiteDateRange(...args: [websiteId: string]) { 1573 | } 1574 | 1575 | async function relationalQuery(websiteId: string) { 1576 | - const { rawQuery, parseFilters } = prisma; 1577 | + const { rawQuery, parseFilters, getISODateSQL } = prisma; 1578 | const { params } = await parseFilters(websiteId, { startDate: new Date(DEFAULT_RESET_DATE) }); 1579 | 1580 | const result = await rawQuery( 1581 | ` 1582 | select 1583 | - min(created_at) as mindate, 1584 | - max(created_at) as maxdate 1585 | + ${getISODateSQL('min(created_at)')} as mindate, 1586 | + ${getISODateSQL('max(created_at)')} as maxdate 1587 | from website_event 1588 | where website_id = {{websiteId::uuid}} 1589 | and created_at >= {{startDate}} 1590 | diff --git a/src/queries/sql/sessions/getSessionData.ts b/src/queries/sql/sessions/getSessionData.ts 1591 | index a3f1e11..4df4745 100644 1592 | --- a/src/queries/sql/sessions/getSessionData.ts 1593 | +++ b/src/queries/sql/sessions/getSessionData.ts 1594 | @@ -10,7 +10,7 @@ export async function getSessionData(...args: [websiteId: string, sessionId: str 1595 | } 1596 | 1597 | async function relationalQuery(websiteId: string, sessionId: string) { 1598 | - const { rawQuery } = prisma; 1599 | + const { rawQuery, getISODateSQL } = prisma; 1600 | 1601 | return rawQuery( 1602 | ` 1603 | @@ -21,8 +21,8 @@ async function relationalQuery(websiteId: string, sessionId: string) { 1604 | data_type as "dataType", 1605 | replace(string_value, '.0000', '') as "stringValue", 1606 | number_value as "numberValue", 1607 | - date_value as "dateValue", 1608 | - created_at as "createdAt" 1609 | + ${getISODateSQL('date_value')} as "dateValue", 1610 | + ${getISODateSQL('created_at')} as "createdAt" 1611 | from session_data 1612 | where website_id = {{websiteId::uuid}} 1613 | and session_id = {{sessionId::uuid}} 1614 | diff --git a/src/queries/sql/sessions/getWebsiteSession.ts b/src/queries/sql/sessions/getWebsiteSession.ts 1615 | index 97850a2..671cc2f 100644 1616 | --- a/src/queries/sql/sessions/getWebsiteSession.ts 1617 | +++ b/src/queries/sql/sessions/getWebsiteSession.ts 1618 | @@ -10,7 +10,7 @@ export async function getWebsiteSession(...args: [websiteId: string, sessionId: 1619 | } 1620 | 1621 | async function relationalQuery(websiteId: string, sessionId: string) { 1622 | - const { rawQuery, getTimestampDiffSQL } = prisma; 1623 | + const { rawQuery, getTimestampDiffSQL, getISODateSQL } = prisma; 1624 | 1625 | return rawQuery( 1626 | ` 1627 | @@ -26,8 +26,8 @@ async function relationalQuery(websiteId: string, sessionId: string) { 1628 | country, 1629 | region, 1630 | city, 1631 | - min(min_time) as "firstAt", 1632 | - max(max_time) as "lastAt", 1633 | + ${getISODateSQL('min(min_time)')} as "firstAt", 1634 | + ${getISODateSQL('max(max_time)')} as "lastAt", 1635 | count(distinct visit_id) as visits, 1636 | sum(views) as views, 1637 | sum(events) as events, 1638 | diff --git a/src/queries/sql/sessions/getWebsiteSessions.ts b/src/queries/sql/sessions/getWebsiteSessions.ts 1639 | index ff70253..dcc2c07 100644 1640 | --- a/src/queries/sql/sessions/getWebsiteSessions.ts 1641 | +++ b/src/queries/sql/sessions/getWebsiteSessions.ts 1642 | @@ -13,7 +13,7 @@ export async function getWebsiteSessions( 1643 | } 1644 | 1645 | async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams: PageParams) { 1646 | - const { pagedRawQuery, parseFilters } = prisma; 1647 | + const { pagedRawQuery, parseFilters, getISODateSQL } = prisma; 1648 | const { search } = pageParams; 1649 | const { filterQuery, params } = await parseFilters(websiteId, { 1650 | ...filters, 1651 | @@ -37,11 +37,11 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar 1652 | session.country, 1653 | session.region, 1654 | session.city, 1655 | - min(website_event.created_at) as "firstAt", 1656 | - max(website_event.created_at) as "lastAt", 1657 | + ${getISODateSQL('min(website_event.created_at)')} as "firstAt", 1658 | + ${getISODateSQL('max(website_event.created_at)')} as "lastAt", 1659 | count(distinct website_event.visit_id) as "visits", 1660 | sum(case when website_event.event_type = 1 then 1 else 0 end) as "views", 1661 | - max(website_event.created_at) as "createdAt" 1662 | + ${getISODateSQL('max(website_event.created_at)')} as "createdAt" 1663 | from website_event 1664 | join session on session.session_id = website_event.session_id 1665 | where website_event.website_id = {{websiteId::uuid}} 1666 | -- 1667 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQLite support for [Umami](https://github.com/umami-software/umami) 2 | This repository contains a patch bringing SQLite support to Umami.\ 3 | Patch is named after the supported Umami released version. 4 | 5 | ## Getting Umami with SQLite 6 | Multiple ways are possible: 7 | 8 | - Latest patched Umami is available in the releases. 9 | 10 | - Patch can be applied manually by placing it in Umami's root and applying from there with:\ 11 | `patch -p1 < version.patch` (reversible with `patch -p1 -R < version.patch`) 12 | 13 | - A pre-built Docker image is available on `ghcr.io/maxime-j/umami-sqlite:latest` 14 | 15 | ## Configuration 16 | Before building, in `.env` configure Umami to use SQLite with\ 17 | `DATABASE_URL=file:`*path* 18 | 19 | An absolute path is recommended `DATABASE_URL=file:/absolute/path/to/database.db` 20 | 21 | With the Docker image, `DATABASE_URL` needs to be set as an env var and should lead to a volume mounted on `/db`.\ 22 | A Compose file and a Kubernetes YAML (designed for Podman use) are available in the [examples](examples). 23 | 24 | ## Could it be officially supported? 25 | Probably not.\ 26 | Each database eventually requires tailoring to match its specificities, or lack of features, like SQLite not having a storage class for dates/times.\ 27 | Supporting it officially could represent a significant amount of time, and it would add another source of issues (they already had a bunch because of their multiple db support). 28 | 29 | ## Added scripts 30 | SQLite specific scripts are added in `scripts/sqlite` folder: 31 | 32 | `vacuum.js` to execute VACUUM command on the database. You might want to run it sometimes: 33 | >-Frequent inserts, updates, and deletes can cause the database file to become fragmented - where data for a single table or index is scattered around the database file. Running VACUUM ensures that each table and index is largely stored contiguously within the database file. In some cases, VACUUM may also reduce the number of partially filled pages in the database, reducing the size of the database file further. 34 | > 35 | >-When content is deleted from an SQLite database, the content is not usually erased but rather the space used to hold the content is marked as being available for reuse. [...] Running VACUUM will clean the database of all traces of deleted content. 36 | 37 | `convert-utm-clid-columns.js` to convert unprocessed data from versions 2.18 and earlier. 38 | -------------------------------------------------------------------------------- /examples/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | umami: 4 | image: ghcr.io/maxime-j/umami-sqlite:latest 5 | ports: 6 | - "3000:3000" 7 | environment: 8 | DATABASE_URL: file:/db/umami.db 9 | APP_SECRET: replace-me-with-a-random-string 10 | volumes: 11 | - umami-db-data:/db 12 | init: true 13 | restart: always 14 | volumes: 15 | umami-db-data: 16 | -------------------------------------------------------------------------------- /examples/umami-pod.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: umami 5 | annotations: 6 | io.podman.annotations.init/app: "TRUE" 7 | spec: 8 | containers: 9 | - name: app 10 | image: ghcr.io/maxime-j/umami-sqlite:latest 11 | ports: 12 | - containerPort: 3000 13 | # not exposed by default, with podman you can at your preference: 14 | # 15 | # use 3000:3000 16 | # either as --publish option of podman kube play 17 | # or as PublishPort option in [Kube] section of a .kube Quadlet unit 18 | # 19 | # uncommment next line (not recommended with Kubernetes) 20 | # - hostPort: 3000 21 | env: 22 | - name: DATABASE_URL 23 | value: "file:/db/umami.db" 24 | - name: APP_SECRET 25 | value: "replace-me-with-a-random-string" 26 | volumeMounts: 27 | - mountPath: /db 28 | name: umami-db-data 29 | volumes: 30 | - name: umami-db-data 31 | persistentVolumeClaim: 32 | claimName: umami-db-data 33 | --------------------------------------------------------------------------------