From a96517caf157e690efde62703b22aec28831f8f0 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 5 Aug 2025 22:06:34 +0200 Subject: [PATCH 1/3] Add device model with relations --- CHANGELOG.md | 5 +++++ app/models/device.rb | 6 ++++++ app/models/point.rb | 3 ++- app/models/user.rb | 1 + app/serializers/point_serializer.rb | 2 +- db/migrate/20250805184854_create_devices.rb | 14 ++++++++++++++ .../20250805184855_add_device_id_to_points.rb | 9 +++++++++ db/schema.rb | 15 ++++++++++++++- spec/factories/devices.rb | 9 +++++++++ spec/factories/points.rb | 1 + spec/models/device_spec.rb | 13 +++++++++++++ spec/models/point_spec.rb | 4 ++++ spec/models/user_spec.rb | 1 + spec/services/photos/importer_spec.rb | 3 ++- 14 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 app/models/device.rb create mode 100644 db/migrate/20250805184854_create_devices.rb create mode 100644 db/migrate/20250805184855_add_device_id_to_points.rb create mode 100644 spec/factories/devices.rb create mode 100644 spec/models/device_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 30f554a6..aaf648f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Possibly fixed a bug where visits were no suggested correctly. #984 - Scratch map is now working correctly. +## Added + +- Internal data structure for separate devices in a single user account. + - [ ] Immich and Photoprism integrations should fill all possible fields in points table + # [0.30.7] - 2025-08-01 diff --git a/app/models/device.rb b/app/models/device.rb new file mode 100644 index 00000000..a1e8b6ac --- /dev/null +++ b/app/models/device.rb @@ -0,0 +1,6 @@ +class Device < ApplicationRecord + belongs_to :user + + validates :name, presence: true + validates :identifier, presence: true, uniqueness: { scope: :user_id } +end diff --git a/app/models/point.rb b/app/models/point.rb index ef00e99b..97513112 100644 --- a/app/models/point.rb +++ b/app/models/point.rb @@ -9,10 +9,11 @@ class Point < ApplicationRecord belongs_to :user belongs_to :country, optional: true belongs_to :track, optional: true + belongs_to :device, optional: true validates :timestamp, :lonlat, presence: true validates :lonlat, uniqueness: { - scope: %i[timestamp user_id], + scope: %i[timestamp user_id device_id], message: 'already has a point at this location and time for this user', index: true } diff --git a/app/models/user.rb b/app/models/user.rb index 4c61d98e..13c5cee6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -15,6 +15,7 @@ class User < ApplicationRecord has_many :places, through: :visits has_many :trips, dependent: :destroy has_many :tracks, dependent: :destroy + has_many :devices, dependent: :destroy after_create :create_api_key after_commit :activate, on: :create, if: -> { DawarichSettings.self_hosted? } diff --git a/app/serializers/point_serializer.rb b/app/serializers/point_serializer.rb index 580dd574..091b7fc5 100644 --- a/app/serializers/point_serializer.rb +++ b/app/serializers/point_serializer.rb @@ -3,7 +3,7 @@ class PointSerializer EXCLUDED_ATTRIBUTES = %w[ created_at updated_at visit_id id import_id user_id raw_data lonlat - reverse_geocoded_at country_id + reverse_geocoded_at country_id device_id ].freeze def initialize(point) diff --git a/db/migrate/20250805184854_create_devices.rb b/db/migrate/20250805184854_create_devices.rb new file mode 100644 index 00000000..6ad17766 --- /dev/null +++ b/db/migrate/20250805184854_create_devices.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CreateDevices < ActiveRecord::Migration[8.0] + def change + create_table :devices do |t| + t.string :name, null: false + t.references :user, null: false, foreign_key: true + t.string :identifier, null: false + + t.timestamps + end + add_index :devices, :identifier + end +end diff --git a/db/migrate/20250805184855_add_device_id_to_points.rb b/db/migrate/20250805184855_add_device_id_to_points.rb new file mode 100644 index 00000000..ff415193 --- /dev/null +++ b/db/migrate/20250805184855_add_device_id_to_points.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddDeviceIdToPoints < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def change + add_reference :points, :device, null: true, index: { algorithm: :concurrently } + end +end diff --git a/db/schema.rb b/db/schema.rb index feac06e4..2021122b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_07_28_191359) do +ActiveRecord::Schema[8.0].define(version: 2025_08_05_184855) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "postgis" @@ -80,6 +80,16 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_28_191359) do create_table "data_migrations", primary_key: "version", id: :string, force: :cascade do |t| end + create_table "devices", force: :cascade do |t| + t.string "name", null: false + t.bigint "user_id", null: false + t.string "identifier", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["identifier"], name: "index_devices_on_identifier" + t.index ["user_id"], name: "index_devices_on_user_id" + end + create_table "exports", force: :cascade do |t| t.string "name", null: false t.string "url" @@ -187,6 +197,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_28_191359) do t.bigint "country_id" t.bigint "track_id" t.string "country_name" + t.bigint "device_id" t.index ["altitude"], name: "index_points_on_altitude" t.index ["battery"], name: "index_points_on_battery" t.index ["battery_status"], name: "index_points_on_battery_status" @@ -195,6 +206,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_28_191359) do t.index ["country"], name: "index_points_on_country" t.index ["country_id"], name: "index_points_on_country_id" t.index ["country_name"], name: "index_points_on_country_name" + t.index ["device_id"], name: "index_points_on_device_id" t.index ["external_track_id"], name: "index_points_on_external_track_id" t.index ["geodata"], name: "index_points_on_geodata", using: :gin t.index ["import_id"], name: "index_points_on_import_id" @@ -300,6 +312,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_28_191359) do add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "areas", "users" + add_foreign_key "devices", "users" add_foreign_key "notifications", "users" add_foreign_key "place_visits", "places" add_foreign_key "place_visits", "visits" diff --git a/spec/factories/devices.rb b/spec/factories/devices.rb new file mode 100644 index 00000000..ac850fe7 --- /dev/null +++ b/spec/factories/devices.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :device do + name { SecureRandom.uuid } + user + identifier { SecureRandom.uuid } + end +end diff --git a/spec/factories/points.rb b/spec/factories/points.rb index acc097e9..a4baf1f6 100644 --- a/spec/factories/points.rb +++ b/spec/factories/points.rb @@ -31,6 +31,7 @@ FactoryBot.define do lonlat { "POINT(#{FFaker::Geolocation.lng} #{FFaker::Geolocation.lat})" } user country_id { nil } + device # Add transient attribute to handle country strings transient do diff --git a/spec/models/device_spec.rb b/spec/models/device_spec.rb new file mode 100644 index 00000000..a4cf3b4e --- /dev/null +++ b/spec/models/device_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Device, type: :model do + describe 'validations' do + subject { build(:device) } + + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_presence_of(:identifier) } + it { is_expected.to validate_uniqueness_of(:identifier).scoped_to(:user_id) } + end +end diff --git a/spec/models/point_spec.rb b/spec/models/point_spec.rb index eaf3d4ba..c2511406 100644 --- a/spec/models/point_spec.rb +++ b/spec/models/point_spec.rb @@ -9,11 +9,15 @@ RSpec.describe Point, type: :model do it { is_expected.to belong_to(:country).optional } it { is_expected.to belong_to(:visit).optional } it { is_expected.to belong_to(:track).optional } + it { is_expected.to belong_to(:device).optional } end describe 'validations' do + subject { build(:point, timestamp: Time.current, lonlat: 'POINT(1.0 2.0)') } + it { is_expected.to validate_presence_of(:timestamp) } it { is_expected.to validate_presence_of(:lonlat) } + it { is_expected.to validate_uniqueness_of(:lonlat).scoped_to(%i[timestamp user_id device_id]).with_message('already has a point at this location and time for this user') } end describe 'callbacks' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3caba416..21410e7f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -15,6 +15,7 @@ RSpec.describe User, type: :model do it { is_expected.to have_many(:places).through(:visits) } it { is_expected.to have_many(:trips).dependent(:destroy) } it { is_expected.to have_many(:tracks).dependent(:destroy) } + it { is_expected.to have_many(:devices).dependent(:destroy) } end describe 'enums' do diff --git a/spec/services/photos/importer_spec.rb b/spec/services/photos/importer_spec.rb index 567898a3..1425f67d 100644 --- a/spec/services/photos/importer_spec.rb +++ b/spec/services/photos/importer_spec.rb @@ -9,6 +9,7 @@ RSpec.describe Photos::Importer do let(:user) do create(:user, settings: { 'immich_url' => 'http://immich.app', 'immich_api_key' => '123456' }) end + let(:device) { create(:device, user:) } let(:immich_data) do JSON.parse(File.read(Rails.root.join('spec/fixtures/files/immich/geodata.json'))) @@ -44,7 +45,7 @@ RSpec.describe Photos::Importer do context 'when there are points with the same coordinates' do let!(:existing_point) do - create(:point, lonlat: 'POINT(30.0000 59.0000)', timestamp: 978_296_400, user:) + create(:point, lonlat: 'POINT(30.0000 59.0000)', timestamp: 978_296_400, user:, device:) end it 'creates only new points' do From a5e07def23108b80f29e0b77af28058a16e07334 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Fri, 8 Aug 2025 01:19:59 +0200 Subject: [PATCH 2/3] Add device support to geodata importers --- CHANGELOG.md | 2 + app/models/concerns/point_validation.rb | 1 + app/services/immich/import_geodata.rb | 3 +- app/services/photoprism/import_geodata.rb | 3 +- app/services/photos/importer.rb | 3 +- spec/fixtures/files/immich/response.json | 2 + spec/models/concerns/point_validation_spec.rb | 53 ++----------------- .../requests/api/v1/countries/borders_spec.rb | 12 ++++- spec/services/photos/importer_spec.rb | 18 ++++++- 9 files changed, 44 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaf648f0..a970503f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Internal data structure for separate devices in a single user account. - [ ] Immich and Photoprism integrations should fill all possible fields in points table +- Geodata from Immich and Photoprism now will also write `tracker_id` to the points table. This will allow to group points by device. It's a good idea to delete your existing imports from Photoprism and Immich and import them again. This will remove existing points and re-import them as long as photos are still available. +- [ ] Add tracker_id index to points table diff --git a/app/models/concerns/point_validation.rb b/app/models/concerns/point_validation.rb index 4bd919d3..f43ee169 100644 --- a/app/models/concerns/point_validation.rb +++ b/app/models/concerns/point_validation.rb @@ -7,6 +7,7 @@ module PointValidation Point.where( lonlat: params[:lonlat], timestamp: params[:timestamp].to_i, + tracker_id: params[:tracker_id], user_id: ).exists? end diff --git a/app/services/immich/import_geodata.rb b/app/services/immich/import_geodata.rb index 9f9679ee..f9106dd9 100644 --- a/app/services/immich/import_geodata.rb +++ b/app/services/immich/import_geodata.rb @@ -56,7 +56,8 @@ class Immich::ImportGeodata latitude: asset['exifInfo']['latitude'], longitude: asset['exifInfo']['longitude'], lonlat: "SRID=4326;POINT(#{asset['exifInfo']['longitude']} #{asset['exifInfo']['latitude']})", - timestamp: Time.zone.parse(asset['exifInfo']['dateTimeOriginal']).to_i + timestamp: Time.zone.parse(asset['exifInfo']['dateTimeOriginal']).to_i, + tracker_id: asset['deviceId'] } end diff --git a/app/services/photoprism/import_geodata.rb b/app/services/photoprism/import_geodata.rb index c31946c1..3e703222 100644 --- a/app/services/photoprism/import_geodata.rb +++ b/app/services/photoprism/import_geodata.rb @@ -66,7 +66,8 @@ class Photoprism::ImportGeodata latitude: asset['Lat'], longitude: asset['Lng'], lonlat: "SRID=4326;POINT(#{asset['Lng']} #{asset['Lat']})", - timestamp: Time.zone.parse(asset['TakenAt']).to_i + timestamp: Time.zone.parse(asset['TakenAt']).to_i, + tracker_id: "#{asset['CameraMake']} #{asset['CameraModel']}" } end diff --git a/app/services/photos/importer.rb b/app/services/photos/importer.rb index f3ce8fc4..4d529bd3 100644 --- a/app/services/photos/importer.rb +++ b/app/services/photos/importer.rb @@ -19,7 +19,7 @@ class Photos::Importer def create_point(point, index) return 0 unless valid?(point) - return 0 if point_exists?(point, point['timestamp']) + return 0 if point_exists?(point, user_id) Point.create( lonlat: point['lonlat'], @@ -28,6 +28,7 @@ class Photos::Importer timestamp: point['timestamp'].to_i, raw_data: point, import_id: import.id, + tracker_id: point['tracker_id'], user_id: ) diff --git a/spec/fixtures/files/immich/response.json b/spec/fixtures/files/immich/response.json index 13eb414d..54a98fbe 100644 --- a/spec/fixtures/files/immich/response.json +++ b/spec/fixtures/files/immich/response.json @@ -3,6 +3,7 @@ { "assets": [ { + "deviceId": "MyString", "exifInfo": { "dateTimeOriginal": "2022-12-31T23:17:06.170Z", "latitude": 52.0000, @@ -10,6 +11,7 @@ } }, { + "deviceId": "MyString", "exifInfo": { "dateTimeOriginal": "2022-12-31T23:21:53.140Z", "latitude": 52.0000, diff --git a/spec/models/concerns/point_validation_spec.rb b/spec/models/concerns/point_validation_spec.rb index b66755fa..71f7e091 100644 --- a/spec/models/concerns/point_validation_spec.rb +++ b/spec/models/concerns/point_validation_spec.rb @@ -104,56 +104,13 @@ RSpec.describe PointValidation do end end - context 'with integration tests', :db do - # These tests require a database with PostGIS support - # Only run them if using real database integration - - let(:existing_timestamp) { 1_650_000_000 } - let(:existing_point_params) do - { - lonlat: 'POINT(10.5 50.5)', - timestamp: existing_timestamp, - user_id: user.id - } + context 'with point existing in device scope' do + let(:existing_point) do + create(:point, lonlat: 'POINT(10.5 50.5)', timestamp: Time.now.to_i, tracker_id: '123', user_id: user.id) end - before do - # Skip this context if not in integration mode - skip 'Skipping integration tests' unless ENV['RUN_INTEGRATION_TESTS'] - - # Create a point in the database - existing_point = Point.create!( - lonlat: "POINT(#{existing_point_params[:longitude]} #{existing_point_params[:latitude]})", - timestamp: existing_timestamp, - user_id: user.id - ) - end - - it 'returns true when a point with same coordinates and timestamp exists' do - params = { - lonlat: 'POINT(10.5 50.5)', - timestamp: existing_timestamp - } - - expect(validator.point_exists?(params, user.id)).to be true - end - - it 'returns false when a point with different coordinates exists' do - params = { - lonlat: 'POINT(10.6 50.5)', - timestamp: existing_timestamp - } - - expect(validator.point_exists?(params, user.id)).to be false - end - - it 'returns false when a point with different timestamp exists' do - params = { - lonlat: 'POINT(10.5 50.5)', - timestamp: existing_timestamp + 1 - } - - expect(validator.point_exists?(params, user.id)).to be false + it 'returns true' do + expect(validator.point_exists?(existing_point, user.id)).to be true end end end diff --git a/spec/requests/api/v1/countries/borders_spec.rb b/spec/requests/api/v1/countries/borders_spec.rb index 1162e198..53a44cf3 100644 --- a/spec/requests/api/v1/countries/borders_spec.rb +++ b/spec/requests/api/v1/countries/borders_spec.rb @@ -4,12 +4,22 @@ require 'rails_helper' RSpec.describe 'Api::V1::Countries::Borders', type: :request do describe 'GET /index' do + let(:user) { create(:user) } + it 'returns a list of countries with borders' do - get '/api/v1/countries/borders' + get '/api/v1/countries/borders', headers: { 'Authorization' => "Bearer #{user.api_key}" } expect(response).to have_http_status(:success) expect(response.body).to include('AF') expect(response.body).to include('ZW') end + + context 'when user is not authenticated' do + it 'returns http unauthorized' do + get '/api/v1/countries/borders' + + expect(response).to have_http_status(:unauthorized) + end + end end end diff --git a/spec/services/photos/importer_spec.rb b/spec/services/photos/importer_spec.rb index 1425f67d..eb0fdd94 100644 --- a/spec/services/photos/importer_spec.rb +++ b/spec/services/photos/importer_spec.rb @@ -45,12 +45,28 @@ RSpec.describe Photos::Importer do context 'when there are points with the same coordinates' do let!(:existing_point) do - create(:point, lonlat: 'POINT(30.0000 59.0000)', timestamp: 978_296_400, user:, device:) + create(:point, + lonlat: 'SRID=4326;POINT(30.0000 59.0000)', + timestamp: 978_296_400, + user: user, + device: device, + tracker_id: nil + ) end it 'creates only new points' do expect { service }.to change { Point.count }.by(1) end + + it 'does not create duplicate points' do + service + points = Point.where( + lonlat: 'SRID=4326;POINT(30.0000 59.0000)', + timestamp: 978_296_400, + user_id: user.id + ) + expect(points.count).to eq(1) + end end end end From bfcd0ac45bbab17a923986f23d8ac4b2e7bfc1bf Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 11 Aug 2025 00:11:28 +0200 Subject: [PATCH 3/3] Add unique index to points with device_id and add tracker_id index --- CHANGELOG.md | 17 +++++++++-------- ...add_unique_index_to_points_with_device_id.rb | 15 +++++++++++++++ ...0810110943_add_index_to_points_tracker_id.rb | 14 ++++++++++++++ ...11002_remove_old_unique_index_from_points.rb | 15 +++++++++++++++ db/schema.rb | 5 +++-- 5 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 db/migrate/20250810110904_add_unique_index_to_points_with_device_id.rb create mode 100644 db/migrate/20250810110943_add_index_to_points_tracker_id.rb create mode 100644 db/migrate/20250810111002_remove_old_unique_index_from_points.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index a970503f..e505d48b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +# [0.30.9] - 2025-08-10 + +## Added + +- Internal data structure for separate devices in a single user account. +- Geodata from Immich and Photoprism now will also write `tracker_id` to the points table. This will allow to group points by device. It's a good idea to delete your existing imports from Photoprism and Immich and import them again. This will remove existing points and re-import them as long as photos are still available. +- [ ] Add tracker_id index to points table + + # [0.30.8] - 2025-08-01 ## Fixed @@ -12,14 +21,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Possibly fixed a bug where visits were no suggested correctly. #984 - Scratch map is now working correctly. -## Added - -- Internal data structure for separate devices in a single user account. - - [ ] Immich and Photoprism integrations should fill all possible fields in points table -- Geodata from Immich and Photoprism now will also write `tracker_id` to the points table. This will allow to group points by device. It's a good idea to delete your existing imports from Photoprism and Immich and import them again. This will remove existing points and re-import them as long as photos are still available. -- [ ] Add tracker_id index to points table - - # [0.30.7] - 2025-08-01 diff --git a/db/migrate/20250810110904_add_unique_index_to_points_with_device_id.rb b/db/migrate/20250810110904_add_unique_index_to_points_with_device_id.rb new file mode 100644 index 00000000..833c171c --- /dev/null +++ b/db/migrate/20250810110904_add_unique_index_to_points_with_device_id.rb @@ -0,0 +1,15 @@ +class AddUniqueIndexToPointsWithDeviceId < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def up + add_index :points, [:lonlat, :timestamp, :user_id, :device_id], + name: "index_points_on_lonlat_timestamp_user_id_device_id", + unique: true, + algorithm: :concurrently, + if_not_exists: true + end + + def down + remove_index :points, name: "index_points_on_lonlat_timestamp_user_id_device_id", algorithm: :concurrently + end +end diff --git a/db/migrate/20250810110943_add_index_to_points_tracker_id.rb b/db/migrate/20250810110943_add_index_to_points_tracker_id.rb new file mode 100644 index 00000000..f7d8307c --- /dev/null +++ b/db/migrate/20250810110943_add_index_to_points_tracker_id.rb @@ -0,0 +1,14 @@ +class AddIndexToPointsTrackerId < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def up + add_index :points, :tracker_id, + name: "index_points_on_tracker_id", + algorithm: :concurrently, + if_not_exists: true + end + + def down + remove_index :points, name: "index_points_on_tracker_id", algorithm: :concurrently + end +end diff --git a/db/migrate/20250810111002_remove_old_unique_index_from_points.rb b/db/migrate/20250810111002_remove_old_unique_index_from_points.rb new file mode 100644 index 00000000..3233ab31 --- /dev/null +++ b/db/migrate/20250810111002_remove_old_unique_index_from_points.rb @@ -0,0 +1,15 @@ +class RemoveOldUniqueIndexFromPoints < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def up + remove_index :points, name: "index_points_on_lonlat_timestamp_user_id", algorithm: :concurrently + end + + def down + add_index :points, [:lonlat, :timestamp, :user_id], + name: "index_points_on_lonlat_timestamp_user_id", + unique: true, + algorithm: :concurrently, + if_not_exists: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 2021122b..f7bb9d85 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_08_05_184855) do +ActiveRecord::Schema[8.0].define(version: 2025_08_10_111002) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "postgis" @@ -211,11 +211,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_05_184855) do t.index ["geodata"], name: "index_points_on_geodata", using: :gin t.index ["import_id"], name: "index_points_on_import_id" t.index ["latitude", "longitude"], name: "index_points_on_latitude_and_longitude" - t.index ["lonlat", "timestamp", "user_id"], name: "index_points_on_lonlat_timestamp_user_id", unique: true + t.index ["lonlat", "timestamp", "user_id", "device_id"], name: "index_points_on_lonlat_timestamp_user_id_device_id", unique: true t.index ["lonlat"], name: "index_points_on_lonlat", using: :gist t.index ["reverse_geocoded_at"], name: "index_points_on_reverse_geocoded_at" t.index ["timestamp"], name: "index_points_on_timestamp" t.index ["track_id"], name: "index_points_on_track_id" + t.index ["tracker_id"], name: "index_points_on_tracker_id" t.index ["trigger"], name: "index_points_on_trigger" t.index ["user_id", "timestamp", "track_id"], name: "idx_points_track_generation" t.index ["user_id"], name: "index_points_on_user_id"