diff --git a/docker/postgresql/docker-compose.yml b/docker/postgresql/docker-compose.yml index cea228ea..f24127b3 100644 --- a/docker/postgresql/docker-compose.yml +++ b/docker/postgresql/docker-compose.yml @@ -3,6 +3,8 @@ services: db: image: 'postgres:17.2-alpine3.21' restart: always + ports: + - 5432:5432 environment: POSTGRES_PASSWORD: 1234 networks: diff --git a/docker/postgresql/start.sh b/docker/postgresql/start.sh old mode 100644 new mode 100755 index a584aad7..9cd3f695 --- a/docker/postgresql/start.sh +++ b/docker/postgresql/start.sh @@ -4,5 +4,10 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd "$SCRIPT_DIR" +# Stop any existing containers and remove them +docker compose down +# Start the services in detached mode +docker compose up -d +echo "PostgreSQL is ready" diff --git a/examples/data/location/location_example b/examples/data/location/location_example new file mode 100755 index 00000000..a8d9e0cd Binary files /dev/null and b/examples/data/location/location_example differ diff --git a/examples/virt/docker/ai_web_ui/readme.md b/examples/virt/docker/ai_web_ui/readme.md index 4fccbc10..0cbf6217 100644 --- a/examples/virt/docker/ai_web_ui/readme.md +++ b/examples/virt/docker/ai_web_ui/readme.md @@ -2,7 +2,7 @@ - make docker build (see docker_ubuntu example) - start from docker_ubuntu - - for build use our vlanf approach (example see docker_ubuntu, make sure we have our zinit & ssh working) + - for build use our vlang approach (example see docker_ubuntu, make sure we have our zinit & ssh working) - install the web UI: openwebui (not by docker but use uv to install this software) - use https://github.com/astral-sh/uv for the python part - as last step, clean it all up (remove apt cache, ...) diff --git a/lib/data/location/db.v b/lib/data/location/db.v index 41f9d348..8b564ccf 100644 --- a/lib/data/location/db.v +++ b/lib/data/location/db.v @@ -1,6 +1,6 @@ module location -import db.sqlite +import db.pg import os import encoding.csv import freeflowuniverse.herolib.osal @@ -9,7 +9,7 @@ import freeflowuniverse.herolib.core.pathlib // LocationDB handles all database operations for locations pub struct LocationDB { mut: - db sqlite.DB + db pg.DB tmp_dir pathlib.Path db_dir pathlib.Path } @@ -17,7 +17,48 @@ mut: // new_location_db creates a new LocationDB instance pub fn new_location_db(reset bool) !LocationDB { mut db_dir := pathlib.get_dir(path:'${os.home_dir()}/hero/var/db/location.db',create: true)! - db := sqlite.connect("${db_dir.path}/locations.db")! + + // PostgreSQL connection parameters with defaults + mut host := os.getenv('POSTGRES_HOST') + if host == '' { + host = 'localhost' + } + port := os.getenv('POSTGRES_PORT') + port_num := if port == '' { 5432 } else { port.int() } + mut user := os.getenv('POSTGRES_USER') + if user == '' { + user = 'postgres' + } + mut password := os.getenv('POSTGRES_PASSWORD') + if password == '' { + password = '1234' + } + mut dbname := os.getenv('POSTGRES_DB') + if dbname == '' { + dbname = 'locations' + } + + // First try to connect to create the database if it doesn't exist + mut init_db := pg.connect( + host: host + port: port_num + user: user + password: password + dbname: 'postgres' + ) or { return error('Failed to connect to PostgreSQL: ${err}') } + + init_db.exec("CREATE DATABASE ${dbname}") or {} + init_db.close() + + // Now connect to our database + db := pg.connect( + host: host + port: port_num + user: user + password: password + dbname: dbname + ) or { return error('Failed to connect to PostgreSQL: ${err}') } + mut loc_db := LocationDB{ db: db tmp_dir: pathlib.get_dir(path: '/tmp/location/',create: true)! @@ -30,9 +71,11 @@ pub fn new_location_db(reset bool) !LocationDB { // init_tables drops and recreates all tables fn (mut l LocationDB) init_tables(reset bool) ! { if reset { - l.db.exec('DROP TABLE IF EXISTS AlternateName')! - l.db.exec('DROP TABLE IF EXISTS City')! - l.db.exec('DROP TABLE IF EXISTS Country')! + sql l.db { + drop table AlternateName + drop table City + drop table Country + }! } sql l.db { @@ -49,5 +92,5 @@ fn (mut l LocationDB) init_tables(reset bool) ! { // close closes the database connection pub fn (mut l LocationDB) close() ! { - l.db.close() or { return err } + l.db.close() } diff --git a/lib/data/location/models.v b/lib/data/location/models.v index 50e9abed..c5b921a4 100644 --- a/lib/data/location/models.v +++ b/lib/data/location/models.v @@ -31,7 +31,7 @@ pub: feature_class string @[max_len: 1] // For filtering (P for populated places) feature_code string @[max_len: 10] // Detailed type (PPL, PPLA, etc.) search_priority int - accuracy u8 = 1 //1=estimated, 4=geonameid, 6=centroid of addresses or shape + accuracy i16 = 1 //1=estimated, 4=geonameid, 6=centroid of addresses or shape } pub struct AlternateName { diff --git a/lib/data/location/readme.md b/lib/data/location/readme.md new file mode 100644 index 00000000..6ba3c5c9 --- /dev/null +++ b/lib/data/location/readme.md @@ -0,0 +1,5 @@ + + +make sure to do + +brew install libpq \ No newline at end of file diff --git a/lib/data/location/search.v b/lib/data/location/search.v index 4b697914..4c276427 100644 --- a/lib/data/location/search.v +++ b/lib/data/location/search.v @@ -1,177 +1,177 @@ module location -import db.sqlite +import db.pg +// // search searches for locations based on the provided options +// pub fn (l Location) search(query string, country_code string, limit int, fuzzy bool) ![]SearchResult { +// opts := SearchOptions{ +// query: query +// country_code: country_code +// limit: limit +// fuzzy: fuzzy +// } +// return l.db.search_locations(opts) +// } -// search searches for locations based on the provided options -pub fn (l Location) search(query string, country_code string, limit int, fuzzy bool) ![]SearchResult { - opts := SearchOptions{ - query: query - country_code: country_code - limit: limit - fuzzy: fuzzy - } - return l.db.search_locations(opts) -} +// // search_near searches for locations near the given coordinates +// pub fn (l Location) search_near(lat f64, lon f64, radius f64, limit int) ![]SearchResult { +// opts := CoordinateSearchOptions{ +// coordinates: Coordinates{ +// latitude: lat +// longitude: lon +// } +// radius: radius +// limit: limit +// } +// return l.db.search_by_coordinates(opts) +// } -// search_near searches for locations near the given coordinates -pub fn (l Location) search_near(lat f64, lon f64, radius f64, limit int) ![]SearchResult { - opts := CoordinateSearchOptions{ - coordinates: Coordinates{ - latitude: lat - longitude: lon - } - radius: radius - limit: limit - } - return l.db.search_by_coordinates(opts) -} +// // search_locations searches for locations based on the provided options +// pub fn (l LocationDB) search_locations(opts SearchOptions) ![]SearchResult { +// mut query_conditions := []string{} +// mut params := []string{} +// if opts.query != '' { +// if opts.fuzzy { +// query_conditions << '(c.name ILIKE $${params.len + 1} OR c.ascii_name ILIKE $${params.len + 2})' +// params << '%${opts.query}%' +// params << '%${opts.query}%' +// } else { +// query_conditions << '(c.name = $${params.len + 1} OR c.ascii_name = $${params.len + 2})' +// params << opts.query +// params << opts.query +// } +// } -// search_locations searches for locations based on the provided options -pub fn (l LocationDB) search_locations(opts SearchOptions) ![]SearchResult { - mut query_conditions := []string{} - mut params := []string{} +// if opts.country_code != '' { +// query_conditions << 'c.country_iso2 = $${params.len + 1}' +// params << opts.country_code +// } - if opts.query != '' { - if opts.fuzzy { - query_conditions << '(c.name LIKE ? OR c.ascii_name LIKE ?)' - params << '%${opts.query}%' - params << '%${opts.query}%' - } else { - query_conditions << '(c.name = ? OR c.ascii_name = ?)' - params << opts.query - params << opts.query - } - } +// where_clause := if query_conditions.len > 0 { 'WHERE ' + query_conditions.join(' AND ') } else { '' } - if opts.country_code != '' { - query_conditions << 'c.country_iso2 = ?' - params << opts.country_code - } +// query := ' +// SELECT c.*, co.* +// FROM City c +// JOIN Country co ON c.country_iso2 = co.iso2 +// ${where_clause} +// ORDER BY c.search_priority DESC, c.population DESC +// LIMIT ${opts.limit} +// ' - where_clause := if query_conditions.len > 0 { 'WHERE ' + query_conditions.join(' AND ') } else { '' } +// rows := l.db.exec_param_many(query, params)! +// mut results := []SearchResult{cap: rows.len} - query := ' - SELECT c.*, co.* - FROM City c - JOIN Country co ON c.country_iso2 = co.iso2 - ${where_clause} - ORDER BY c.search_priority DESC, c.population DESC - LIMIT ${opts.limit} - ' +// for row in rows { +// city := City{ +// id: row.vals[0].int() or { 0 } +// name: row.vals[1] or { '' } +// ascii_name: row.vals[2] or { '' } +// country_iso2: row.vals[3] or { '' } +// postal_code: row.vals[4] or { '' } +// state_name: row.vals[5] or { '' } +// state_code: row.vals[6] or { '' } +// county_name: row.vals[7] or { '' } +// county_code: row.vals[8] or { '' } +// community_name: row.vals[9] or { '' } +// community_code: row.vals[10] or { '' } +// latitude: row.vals[11].f64() or { 0.0 } +// longitude: row.vals[12].f64() or { 0.0 } +// population: row.vals[13].i64() or { 0 } +// timezone: row.vals[14] or { '' } +// feature_class: row.vals[15] or { '' } +// feature_code: row.vals[16] or { '' } +// search_priority: row.vals[17].int() or { 0 } +// accuracy: u8(row.vals[18].int() or { 1 }) +// } - rows := l.db.exec_param_many(query, params)! - mut results := []SearchResult{cap: rows.len} +// country := Country{ +// iso2: row.vals[19] or { '' } +// name: row.vals[20] or { '' } +// iso3: row.vals[21] or { '' } +// continent: row.vals[22] or { '' } +// population: row.vals[23].i64() or { 0 } +// timezone: row.vals[24] or { '' } +// import_date: row.vals[25].i64() or { 0 } +// } - for row in rows { - city := City{ - id: row.vals[0].int() - name: row.vals[1] - ascii_name: row.vals[2] - country_iso2: row.vals[3] - postal_code: row.vals[4] - state_name: row.vals[5] - state_code: row.vals[6] - county_name: row.vals[7] - county_code: row.vals[8] - community_name: row.vals[9] - community_code: row.vals[10] - latitude: row.vals[11].f64() - longitude: row.vals[12].f64() - population: row.vals[13].i64() - timezone: row.vals[14] - feature_class: row.vals[15] - feature_code: row.vals[16] - search_priority: row.vals[17].int() - accuracy: row.vals[18].u8() - } +// results << SearchResult{ +// city: city +// country: country +// similarity: 1.0 // TODO: implement proper similarity scoring +// } +// } - country := Country{ - iso2: row.vals[19] - name: row.vals[20] - iso3: row.vals[21] - continent: row.vals[22] - population: row.vals[23].i64() - timezone: row.vals[24] - import_date: row.vals[25].i64() - } +// return results +// } - results << SearchResult{ - city: city - country: country - similarity: 1.0 // TODO: implement proper similarity scoring - } - } - - return results -} - -// search_by_coordinates finds locations near the given coordinates -pub fn (l LocationDB) search_by_coordinates(opts CoordinateSearchOptions) ![]SearchResult { - // Use the Haversine formula to calculate distances - query := " - SELECT c.*, co.*, - (6371 * acos(cos(radians(?)) * cos(radians(latitude)) * - cos(radians(longitude) - radians(?)) + sin(radians(?)) * - sin(radians(latitude)))) AS distance - FROM City c - JOIN Country co ON c.country_iso2 = co.iso2 - HAVING distance < ? - ORDER BY distance - LIMIT ? - " +// // search_by_coordinates finds locations near the given coordinates +// pub fn (l LocationDB) search_by_coordinates(opts CoordinateSearchOptions) ![]SearchResult { +// // Use the Haversine formula to calculate distances +// query := " +// WITH distances AS ( +// SELECT c.*, co.*, +// (6371 * acos(cos(radians($1)) * cos(radians(latitude)) * +// cos(radians(longitude) - radians($2)) + sin(radians($1)) * +// sin(radians(latitude)))) AS distance +// FROM City c +// JOIN Country co ON c.country_iso2 = co.iso2 +// ) +// SELECT * FROM distances +// WHERE distance < $3 +// ORDER BY distance +// LIMIT $4 +// " - params := [ - opts.coordinates.latitude.str(), - opts.coordinates.longitude.str(), - opts.coordinates.latitude.str(), - opts.radius.str(), - opts.limit.str() - ] - rows := l.db.exec_param_many(query, params)! +// params := [ +// opts.coordinates.latitude.str(), +// opts.coordinates.longitude.str(), +// opts.radius.str(), +// opts.limit.str() +// ] +// rows := l.db.exec_param_many(query, params)! - mut results := []SearchResult{cap: rows.len} +// mut results := []SearchResult{cap: rows.len} - for row in rows { - city := City{ - id: row.vals[0].int() - name: row.vals[1] - ascii_name: row.vals[2] - country_iso2: row.vals[3] - postal_code: row.vals[4] - state_name: row.vals[5] - state_code: row.vals[6] - county_name: row.vals[7] - county_code: row.vals[8] - community_name: row.vals[9] - community_code: row.vals[10] - latitude: row.vals[11].f64() - longitude: row.vals[12].f64() - population: row.vals[13].i64() - timezone: row.vals[14] - feature_class: row.vals[15] - feature_code: row.vals[16] - search_priority: row.vals[17].int() - accuracy: row.vals[18].u8() - } +// for row in rows { +// city := City{ +// id: row.vals[0].int() or { 0 } +// name: row.vals[1] or { '' } +// ascii_name: row.vals[2] or { '' } +// country_iso2: row.vals[3] or { '' } +// postal_code: row.vals[4] or { '' } +// state_name: row.vals[5] or { '' } +// state_code: row.vals[6] or { '' } +// county_name: row.vals[7] or { '' } +// county_code: row.vals[8] or { '' } +// community_name: row.vals[9] or { '' } +// community_code: row.vals[10] or { '' } +// latitude: row.vals[11].f64() or { 0.0 } +// longitude: row.vals[12].f64() or { 0.0 } +// population: row.vals[13].i64() or { 0 } +// timezone: row.vals[14] or { '' } +// feature_class: row.vals[15] or { '' } +// feature_code: row.vals[16] or { '' } +// search_priority: row.vals[17].int() or { 0 } +// accuracy: u8(row.vals[18].int() or { 1 }) +// } - country := Country{ - iso2: row.vals[19] - name: row.vals[20] - iso3: row.vals[21] - continent: row.vals[22] - population: row.vals[23].i64() - timezone: row.vals[24] - import_date: row.vals[25].i64() - } +// country := Country{ +// iso2: row.vals[19] or { '' } +// name: row.vals[20] or { '' } +// iso3: row.vals[21] or { '' } +// continent: row.vals[22] or { '' } +// population: row.vals[23].i64() or { 0 } +// timezone: row.vals[24] or { '' } +// import_date: row.vals[25].i64() or { 0 } +// } - results << SearchResult{ - city: city - country: country - similarity: 1.0 - } - } +// results << SearchResult{ +// city: city +// country: country +// similarity: 1.0 +// } +// } - return results -} +// return results +// }