added sqlite demo
This commit is contained in:
parent
9080196f6b
commit
cd5d269483
16
demo/sqlite/countries.sql
Normal file
16
demo/sqlite/countries.sql
Normal file
@ -0,0 +1,16 @@
|
||||
.headers on
|
||||
.mode table
|
||||
|
||||
WITH raw AS (
|
||||
SELECT
|
||||
value AS country
|
||||
FROM json_each(readfile('j_countries.json'))
|
||||
)
|
||||
SELECT
|
||||
json_extract(country, '$.translations.deu.common') AS german_name,
|
||||
json_extract(country, '$.population') AS population,
|
||||
json_extract(country, '$.capital[0]') AS capital,
|
||||
json_extract(country, '$.tld[0]') AS tld,
|
||||
json_extract(country, '$.timezones') AS timezones
|
||||
FROM raw
|
||||
ORDER BY german_name;
|
||||
19
demo/sqlite/export_csv_countries.sql
Normal file
19
demo/sqlite/export_csv_countries.sql
Normal file
@ -0,0 +1,19 @@
|
||||
.mode csv
|
||||
.headers on
|
||||
.output result.csv
|
||||
|
||||
WITH raw AS (
|
||||
SELECT
|
||||
value AS country
|
||||
FROM json_each(readfile('j_countries.json'))
|
||||
)
|
||||
|
||||
SELECT
|
||||
json_extract(country, '$.translations.deu.common') AS german_name,
|
||||
json_extract(country, '$.population') AS population,
|
||||
json_extract(country, '$.capital[0]') AS capital,
|
||||
json_extract(country, '$.timezones[0]') AS timezones
|
||||
FROM raw
|
||||
ORDER BY german_name;
|
||||
|
||||
.output stdout
|
||||
1
demo/sqlite/j_countries.json
Normal file
1
demo/sqlite/j_countries.json
Normal file
File diff suppressed because one or more lines are too long
1
demo/sqlite/readfile.sql
Normal file
1
demo/sqlite/readfile.sql
Normal file
@ -0,0 +1 @@
|
||||
SELECT * FROM json_each(readfile('j_countries.json'));
|
||||
196
demo/sqlite/result.csv
Normal file
196
demo/sqlite/result.csv
Normal file
@ -0,0 +1,196 @@
|
||||
german_name,population,capital,timezones
|
||||
Afghanistan,40218234,Kabul,UTC+04:30
|
||||
Albanien,2837743,Tirana,UTC+01:00
|
||||
Algerien,44700000,Algiers,UTC+01:00
|
||||
Andorra,77265,"Andorra la Vella",UTC+01:00
|
||||
Angola,32866268,Luanda,UTC+01:00
|
||||
"Antigua und Barbuda",97928,"Saint John's",UTC-04:00
|
||||
Argentinien,45376763,"Buenos Aires",UTC-03:00
|
||||
Armenien,2963234,Yerevan,UTC+04:00
|
||||
Aserbaidschan,10110116,Baku,UTC+04:00
|
||||
Australien,25687041,Canberra,UTC+05:00
|
||||
Bahamas,393248,Nassau,UTC-05:00
|
||||
Bahrain,1701583,Manama,UTC+03:00
|
||||
Bangladesch,164689383,Dhaka,UTC+06:00
|
||||
Barbados,287371,Bridgetown,UTC-04:00
|
||||
Belgien,11555997,Brussels,UTC+01:00
|
||||
Belize,397621,Belmopan,UTC-06:00
|
||||
Benin,12123198,Porto-Novo,UTC+01:00
|
||||
Bhutan,771612,Thimphu,UTC+06:00
|
||||
Bolivien,11673029,Sucre,UTC-04:00
|
||||
"Bosnien und Herzegowina",3280815,Sarajevo,UTC+01:00
|
||||
Botswana,2351625,Gaborone,UTC+02:00
|
||||
Brasilien,212559409,"Brasília",UTC-05:00
|
||||
Brunei,437483,"Bandar Seri Begawan",UTC+08:00
|
||||
Bulgarien,6927288,Sofia,UTC+02:00
|
||||
"Burkina Faso",20903278,Ouagadougou,UTC
|
||||
Burundi,11890781,Gitega,UTC+02:00
|
||||
Chile,19116209,Santiago,UTC-06:00
|
||||
China,1402112000,Beijing,UTC+08:00
|
||||
"Costa Rica",5094114,"San José",UTC-06:00
|
||||
Deutschland,83240525,Berlin,UTC+01:00
|
||||
Dominica,71991,Roseau,UTC-04:00
|
||||
"Dominikanische Republik",10847904,"Santo Domingo",UTC-04:00
|
||||
Dschibuti,988002,Djibouti,UTC+03:00
|
||||
"Dänemark",5831404,Copenhagen,UTC-04:00
|
||||
Ecuador,17643060,Quito,UTC-06:00
|
||||
"El Salvador",6486201,"San Salvador",UTC-06:00
|
||||
"Elfenbeinküste",26378275,Yamoussoukro,UTC
|
||||
Eritrea,5352000,Asmara,UTC+03:00
|
||||
Estland,1331057,Tallinn,UTC+02:00
|
||||
Fidschi,896444,Suva,UTC+12:00
|
||||
Finnland,5530719,Helsinki,UTC+02:00
|
||||
Frankreich,67391582,Paris,UTC-10:00
|
||||
Gabun,2225728,Libreville,UTC+01:00
|
||||
Gambia,2416664,Banjul,UTC+00:00
|
||||
Georgien,3714000,Tbilisi,UTC+04:00
|
||||
Ghana,31072945,Accra,UTC
|
||||
Grenada,112519,"St. George's",UTC-04:00
|
||||
Griechenland,10715549,Athens,UTC+02:00
|
||||
Guatemala,16858333,"Guatemala City",UTC-06:00
|
||||
Guinea,13132792,Conakry,UTC
|
||||
Guinea-Bissau,1967998,Bissau,UTC
|
||||
Guyana,786559,Georgetown,UTC-04:00
|
||||
Haiti,11402533,Port-au-Prince,UTC-05:00
|
||||
Honduras,9904608,Tegucigalpa,UTC-06:00
|
||||
Indien,1380004385,"New Delhi",UTC+05:30
|
||||
Indonesien,273523621,Jakarta,UTC+07:00
|
||||
Irak,40222503,Baghdad,UTC+03:00
|
||||
Iran,83992953,Tehran,UTC+03:30
|
||||
Irland,4994724,Dublin,UTC
|
||||
Island,366425,Reykjavik,UTC
|
||||
Israel,9216900,Jerusalem,UTC+02:00
|
||||
Italien,59554023,Rome,UTC+01:00
|
||||
Jamaika,2961161,Kingston,UTC-05:00
|
||||
Japan,125836021,Tokyo,UTC+09:00
|
||||
Jemen,29825968,"Sana'a",UTC+03:00
|
||||
Jordanien,10203140,Amman,UTC+03:00
|
||||
Kambodscha,16718971,"Phnom Penh",UTC+07:00
|
||||
Kamerun,26545864,"Yaoundé",UTC+01:00
|
||||
Kanada,38005238,Ottawa,UTC-08:00
|
||||
"Kap Verde",555988,Praia,UTC-01:00
|
||||
Kasachstan,18754440,Astana,UTC+05:00
|
||||
Katar,2881060,Doha,UTC+03:00
|
||||
Kenia,53771300,Nairobi,UTC+03:00
|
||||
Kirgisistan,6591600,Bishkek,UTC+06:00
|
||||
Kiribati,119446,"South Tarawa",UTC+12:00
|
||||
Kolumbien,50882884,"Bogotá",UTC-05:00
|
||||
Komoren,869595,Moroni,UTC+03:00
|
||||
Kongo,5657000,Brazzaville,UTC+01:00
|
||||
"Kongo (Dem. Rep.)",108407721,Kinshasa,UTC+01:00
|
||||
Kosovo,1775378,Pristina,UTC+01:00
|
||||
Kroatien,4047200,Zagreb,UTC+01:00
|
||||
Kuba,11326616,Havana,UTC-05:00
|
||||
Kuwait,4270563,"Kuwait City",UTC+03:00
|
||||
Laos,7275556,Vientiane,UTC+07:00
|
||||
Lesotho,2142252,Maseru,UTC+02:00
|
||||
Lettland,1901548,Riga,UTC+02:00
|
||||
Libanon,6825442,Beirut,UTC+02:00
|
||||
Liberia,5057677,Monrovia,UTC
|
||||
Libyen,6871287,Tripoli,UTC+01:00
|
||||
Liechtenstein,38137,Vaduz,UTC+01:00
|
||||
Litauen,2794700,Vilnius,UTC+02:00
|
||||
Luxemburg,632275,Luxembourg,UTC+01:00
|
||||
Madagaskar,27691019,Antananarivo,UTC+03:00
|
||||
Malawi,19129955,Lilongwe,UTC+02:00
|
||||
Malaysia,32365998,"Kuala Lumpur",UTC+08:00
|
||||
Malediven,540542,"Malé",UTC+05:00
|
||||
Mali,20250834,Bamako,UTC
|
||||
Malta,525285,Valletta,UTC+01:00
|
||||
Marokko,36910558,Rabat,UTC
|
||||
Marshallinseln,59194,Majuro,UTC+12:00
|
||||
Mauretanien,4649660,Nouakchott,UTC
|
||||
Mauritius,1265740,"Port Louis",UTC+04:00
|
||||
Mexiko,128932753,"Mexico City",UTC-08:00
|
||||
Mikronesien,115021,Palikir,UTC+10:00
|
||||
Moldawien,2617820,"Chișinău",UTC+02:00
|
||||
Monaco,39244,Monaco,UTC+01:00
|
||||
Mongolei,3278292,"Ulan Bator",UTC+07:00
|
||||
Montenegro,621718,Podgorica,UTC+01:00
|
||||
Mosambik,31255435,Maputo,UTC+02:00
|
||||
Myanmar,54409794,Naypyidaw,UTC+06:30
|
||||
Namibia,2540916,Windhoek,UTC+01:00
|
||||
Nauru,10834,Yaren,UTC+12:00
|
||||
Nepal,29136808,Kathmandu,UTC+05:45
|
||||
Neuseeland,5084300,Wellington,UTC-11:00
|
||||
Nicaragua,6624554,Managua,UTC-06:00
|
||||
Niederlande,16655799,Amsterdam,UTC+01:00
|
||||
Niger,24206636,Niamey,UTC+01:00
|
||||
Nigeria,206139587,Abuja,UTC+01:00
|
||||
Nordkorea,25778815,Pyongyang,UTC+09:00
|
||||
Nordmazedonien,2077132,Skopje,UTC+01:00
|
||||
Norwegen,5379475,Oslo,UTC+01:00
|
||||
Oman,5106622,Muscat,UTC+04:00
|
||||
Osttimor,1318442,Dili,UTC+09:00
|
||||
Pakistan,220892331,Islamabad,UTC+05:00
|
||||
Palau,18092,Ngerulmud,UTC+09:00
|
||||
Panama,4314768,"Panama City",UTC-05:00
|
||||
Papua-Neuguinea,8947027,"Port Moresby",UTC+10:00
|
||||
Paraguay,7132530,"Asunción",UTC-04:00
|
||||
Peru,32971846,Lima,UTC-05:00
|
||||
Philippinen,109581085,Manila,UTC+08:00
|
||||
Polen,37950802,Warsaw,UTC+01:00
|
||||
Portugal,10305564,Lisbon,UTC-01:00
|
||||
Ruanda,12952209,Kigali,UTC+02:00
|
||||
"Rumänien",19286123,Bucharest,UTC+02:00
|
||||
Russland,144104080,Moscow,UTC+03:00
|
||||
Salomonen,686878,Honiara,UTC+11:00
|
||||
Sambia,18383956,Lusaka,UTC+02:00
|
||||
Samoa,198410,Apia,UTC+13:00
|
||||
"San Marino",33938,"City of San Marino",UTC+01:00
|
||||
Saudi-Arabien,34813867,Riyadh,UTC+03:00
|
||||
Schweden,10353442,Stockholm,UTC+01:00
|
||||
Schweiz,8654622,Bern,UTC+01:00
|
||||
Senegal,16743930,Dakar,UTC
|
||||
Serbien,6908224,Belgrade,UTC+01:00
|
||||
Seychellen,98462,Victoria,UTC+04:00
|
||||
"Sierra Leone",7976985,Freetown,UTC
|
||||
Simbabwe,14862927,Harare,UTC+02:00
|
||||
Singapur,5685807,Singapore,UTC+08:00
|
||||
Slowakei,5458827,Bratislava,UTC+01:00
|
||||
Slowenien,2100126,Ljubljana,UTC+01:00
|
||||
Somalia,15893219,Mogadishu,UTC+03:00
|
||||
Spanien,47351567,Madrid,UTC
|
||||
"Sri Lanka",21919000,"Sri Jayawardenepura Kotte",UTC+05:30
|
||||
"St. Kitts und Nevis",53192,Basseterre,UTC-04:00
|
||||
"St. Lucia",183629,Castries,UTC-04:00
|
||||
"St. Vincent und die Grenadinen",110947,Kingstown,UTC-04:00
|
||||
Sudan,43849269,Khartoum,UTC+03:00
|
||||
Suriname,586634,Paramaribo,UTC-03:00
|
||||
Swasiland,1160164,Mbabane,UTC+02:00
|
||||
Syrien,17500657,Damascus,UTC+02:00
|
||||
"São Tomé und Príncipe",219161,"São Tomé",UTC
|
||||
"Südafrika",59308690,Pretoria,UTC+02:00
|
||||
"Südkorea",51780579,Seoul,UTC+09:00
|
||||
"Südsudan",11193729,Juba,UTC+03:00
|
||||
Tadschikistan,9537642,Dushanbe,UTC+05:00
|
||||
Tansania,59734213,Dodoma,UTC+03:00
|
||||
Thailand,69799978,Bangkok,UTC+07:00
|
||||
Togo,8278737,"Lomé",UTC
|
||||
Tonga,105697,"Nuku'alofa",UTC+13:00
|
||||
"Trinidad und Tobago",1399491,"Port of Spain",UTC-04:00
|
||||
Tschad,16425859,"N'Djamena",UTC+01:00
|
||||
Tschechien,10698896,Prague,UTC+01:00
|
||||
Tunesien,11818618,Tunis,UTC+01:00
|
||||
Turkmenistan,6031187,Ashgabat,UTC+05:00
|
||||
Tuvalu,11792,Funafuti,UTC+12:00
|
||||
"Türkei",84339067,Ankara,UTC+03:00
|
||||
Uganda,45741000,Kampala,UTC+03:00
|
||||
Ukraine,44134693,Kyiv,UTC+02:00
|
||||
Ungarn,9749763,Budapest,UTC+01:00
|
||||
Uruguay,3473727,Montevideo,UTC-03:00
|
||||
Usbekistan,34232050,Tashkent,UTC+05:00
|
||||
Vanuatu,307150,"Port Vila",UTC+11:00
|
||||
Vatikanstadt,451,"Vatican City",UTC+01:00
|
||||
Venezuela,28435943,Caracas,UTC-04:00
|
||||
"Vereinigte Arabische Emirate",9890400,"Abu Dhabi",UTC+04:00
|
||||
"Vereinigte Staaten",329484123,"Washington, D.C.",UTC-12:00
|
||||
"Vereinigtes Königreich",67215293,London,UTC-08:00
|
||||
Vietnam,97338583,Hanoi,UTC+07:00
|
||||
"Weißrussland",9398861,Minsk,UTC+03:00
|
||||
"Zentralafrikanische Republik",4829764,Bangui,UTC+01:00
|
||||
Zypern,1207361,Nicosia,UTC+02:00
|
||||
"Ägypten",102334403,Cairo,UTC+02:00
|
||||
"Äquatorialguinea",1402985,Malabo,UTC+01:00
|
||||
"Äthiopien",114963583,"Addis Ababa",UTC+03:00
|
||||
"Österreich",8917205,Vienna,UTC+01:00
|
||||
|
70
demo/sqlite/sudoku.sql
Normal file
70
demo/sqlite/sudoku.sql
Normal file
@ -0,0 +1,70 @@
|
||||
WITH RECURSIVE
|
||||
input(sud) AS (
|
||||
VALUES('53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5....8..79')
|
||||
),
|
||||
|
||||
digits(z, lp) AS (
|
||||
VALUES('1', 1)
|
||||
UNION ALL
|
||||
SELECT CAST(lp+1 AS TEXT), lp+1 FROM digits WHERE lp<9
|
||||
),
|
||||
|
||||
x(s, ind) AS (
|
||||
SELECT sud, instr(sud, '.') FROM input
|
||||
UNION ALL
|
||||
SELECT
|
||||
substr(s, 1, ind-1) || z || substr(s, ind+1),
|
||||
instr(substr(s, 1, ind-1) || z || substr(s, ind+1), '.')
|
||||
FROM x, digits AS z
|
||||
WHERE ind>0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM digits AS lp
|
||||
WHERE z.z = substr(s, ((ind-1)/9)*9 + lp, 1)
|
||||
OR z.z = substr(s, ((ind-1)%9) + (lp-1)*9 + 1, 1)
|
||||
OR z.z = substr(s, (((ind-1)/3) % 3)*3
|
||||
+ ((ind-1)/27)*27
|
||||
+ lp + ((lp-1)/3)*6, 1)
|
||||
)
|
||||
),
|
||||
|
||||
solved(s) AS (
|
||||
SELECT s FROM x WHERE ind=0
|
||||
),
|
||||
|
||||
-- Build 9 pretty rows for both input and solved using digits.lp = 1..9
|
||||
grid_rows(name, rn, line) AS (
|
||||
SELECT 'input', d.lp,
|
||||
substr(sud, (d.lp-1)*9+1, 3)
|
||||
|| ' | ' ||
|
||||
substr(sud, (d.lp-1)*9+4, 3)
|
||||
|| ' | ' ||
|
||||
substr(sud, (d.lp-1)*9+7, 3)
|
||||
FROM input CROSS JOIN digits d
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT 'solved', d.lp,
|
||||
substr(s, (d.lp-1)*9+1, 3)
|
||||
|| ' | ' ||
|
||||
substr(s, (d.lp-1)*9+4, 3)
|
||||
|| ' | ' ||
|
||||
substr(s, (d.lp-1)*9+7, 3)
|
||||
FROM solved CROSS JOIN digits d
|
||||
),
|
||||
|
||||
-- Insert horizontal separators after rows 3 and 6 by computing an ordering key
|
||||
grid_with_sep AS (
|
||||
SELECT name, rn*2-1 AS ord, line FROM grid_rows
|
||||
UNION ALL
|
||||
SELECT name, rn*2 AS ord, '----+-----+----' FROM grid_rows WHERE rn IN (3,6)
|
||||
)
|
||||
|
||||
-- Single final UNIONed selection, ordered by group + position
|
||||
SELECT line FROM (
|
||||
SELECT 1 AS grp, ord AS ord, line FROM grid_with_sep WHERE name='input'
|
||||
UNION ALL
|
||||
SELECT 2 AS grp, 0 AS ord, '' AS line
|
||||
UNION ALL
|
||||
SELECT 3 AS grp, ord AS ord, line FROM grid_with_sep WHERE name='solved'
|
||||
) ORDER BY grp, ord;
|
||||
14
lcars_v2/embed/create_state_db.sql
Normal file
14
lcars_v2/embed/create_state_db.sql
Normal file
@ -0,0 +1,14 @@
|
||||
CREATE TABLE ship_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp TEXT NOT NULL,
|
||||
subsystem TEXT NOT NULL,
|
||||
severity TEXT CHECK(severity IN ('CRITICAL', 'ALERT', 'WARNING', 'NOTICE', 'INFO')) NOT NULL DEFAULT 'INFO',
|
||||
color TEXT NOT NULL,
|
||||
message TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE crew_member (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rank TEXT NOT NULL,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
95
lcars_v2/embed/crew.json
Normal file
95
lcars_v2/embed/crew.json
Normal file
@ -0,0 +1,95 @@
|
||||
[
|
||||
"Ensign Beckett Mariner",
|
||||
"Ensign Brad Boimler",
|
||||
"Captain Carol Freeman",
|
||||
"Commander Jack Ransom",
|
||||
"Ensign Barsa Orsino",
|
||||
"Lieutenant Commander D'Vana Tendi",
|
||||
"Lieutenant Commander Sam Rutherford",
|
||||
"Lieutenant Commander Shaxs",
|
||||
"Ensign Liora Vance",
|
||||
"Ensign Rylan Sato",
|
||||
"Lieutenant Jarek Torin",
|
||||
"Lieutenant Kira Dallin",
|
||||
"Lieutenant T'Lara Venn",
|
||||
"Lieutenant Shonnie Velar",
|
||||
"Lieutenant Commander Aric Thorne",
|
||||
"Lieutenant Commander Selene Marvik",
|
||||
"Lieutenant Commander Jovan Kreel",
|
||||
"Lieutenant Orin Kallis",
|
||||
"Ensign Mira Talon",
|
||||
"Ensign Fynn Darvik",
|
||||
"Lieutenant Commander Elara Voss",
|
||||
"Lieutenant Zev Ralyn",
|
||||
"Ensign Daxia Morn",
|
||||
"Lieutenant Varek Solis",
|
||||
"Ensign Tylen Kael",
|
||||
"Lieutenant Commander Nira Falco",
|
||||
"Lieutenant Kael Dorran",
|
||||
"Ensign Saren Vale",
|
||||
"Ensign Tova Lin",
|
||||
"Lieutenant Commander Ryn Talor",
|
||||
"Lieutenant Draven Korr",
|
||||
"Ensign Lyra Kenning",
|
||||
"Ensign Joren Pax",
|
||||
"Lieutenant Commander Calix Arden",
|
||||
"Lieutenant Selan Vey",
|
||||
"Ensign Aricel Taren",
|
||||
"Ensign Velin Daro",
|
||||
"Lieutenant Caris Vennor",
|
||||
"Lieutenant Kellen Dray",
|
||||
"Lieutenant Risa Talven",
|
||||
"Lieutenant Commander Thalen Voss",
|
||||
"Lieutenant Commander Sariah Quell",
|
||||
"Ensign Orin Talvik",
|
||||
"Ensign Lyric Selden",
|
||||
"Lieutenant Commander Varen Korr",
|
||||
"Lieutenant Elara Vynn",
|
||||
"Ensign Jax Talmar",
|
||||
"Lieutenant Commander Neris Vay",
|
||||
"Lieutenant Draven Solis",
|
||||
"Ensign Tavia Korlen",
|
||||
"Ensign Ryn Paxil",
|
||||
"Lieutenant Commander Kira Dalen",
|
||||
"Lieutenant Zev Ardin",
|
||||
"Ensign Lyra Taven",
|
||||
"Ensign Fynn Velar",
|
||||
"Lieutenant Commander Calen Rhos",
|
||||
"Lieutenant Selan Vaylen",
|
||||
"Ensign Aricel Dorran",
|
||||
"Ensign Tylen Korr",
|
||||
"Lieutenant Commander Nira Talos",
|
||||
"Lieutenant Kael Venn",
|
||||
"Ensign Saren Daro",
|
||||
"Ensign Tova Vennor",
|
||||
"Lieutenant Commander Ryn Arden",
|
||||
"Lieutenant Draven Voss",
|
||||
"Ensign Lyra Talin",
|
||||
"Ensign Joren Vay",
|
||||
"Lieutenant Commander Calix Talven",
|
||||
"Lieutenant Selan Korr",
|
||||
"Ensign Aricel Vynn",
|
||||
"Ensign Velin Talor",
|
||||
"Lieutenant Caris Vaylen",
|
||||
"Lieutenant Kellen Rhos",
|
||||
"Lieutenant Risa Vennor",
|
||||
"Lieutenant Commander Thalen Daro",
|
||||
"Lieutenant Commander Sariah Voss",
|
||||
"Ensign Orin Vaylen",
|
||||
"Ensign Lyric Talven",
|
||||
"Lieutenant Commander Varen Talos",
|
||||
"Lieutenant Elara Solis",
|
||||
"Ensign Jax Vennor",
|
||||
"Lieutenant Commander Neris Talor",
|
||||
"Lieutenant Draven Vaylen",
|
||||
"Ensign Tavia Dorran",
|
||||
"Ensign Ryn Voss",
|
||||
"Lieutenant Commander Kira Arden",
|
||||
"Lieutenant Zev Vay",
|
||||
"Ensign Lyra Daro",
|
||||
"Ensign Fynn Talor",
|
||||
"Lieutenant Commander Calen Venn",
|
||||
"Lieutenant Selan Talvik",
|
||||
"Ensign Aricel Rhos"
|
||||
]
|
||||
|
||||
1066
lcars_v2/embed/messages.json
Normal file
1066
lcars_v2/embed/messages.json
Normal file
File diff suppressed because it is too large
Load Diff
312
lcars_v2/eventbus/eventbus.go
Normal file
312
lcars_v2/eventbus/eventbus.go
Normal file
@ -0,0 +1,312 @@
|
||||
// modified version, see original:
|
||||
// https://github.com/dtomasi/go-event-bus/blob/main/event_bus.go
|
||||
|
||||
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Rec map[string]interface{}
|
||||
|
||||
|
||||
// Event holds topic name and data.
|
||||
type Event struct {
|
||||
Data Rec
|
||||
Topic string
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// Done calls Done on sync.WaitGroup if set.
|
||||
func (e *Event) Done() {
|
||||
if e.wg != nil {
|
||||
e.wg.Done()
|
||||
}
|
||||
}
|
||||
|
||||
// CallbackFunc Defines a CallbackFunc.
|
||||
type CallbackFunc func(topic string, data Rec)
|
||||
|
||||
// EventChannel is a channel which can accept an Event.
|
||||
type EventChannel chan Event
|
||||
|
||||
// NewEventChannel Creates a new EventChannel.
|
||||
func NewEventChannel() EventChannel {
|
||||
return make(EventChannel)
|
||||
}
|
||||
|
||||
// dataChannelSlice is a slice of DataChannels.
|
||||
type eventChannelSlice []EventChannel
|
||||
|
||||
// EventBus stores the information about subscribers interested for a particular topic.
|
||||
type EventBus struct {
|
||||
mu sync.RWMutex
|
||||
subscribers map[string]eventChannelSlice
|
||||
stats *Stats
|
||||
}
|
||||
|
||||
// NewEventBus returns a new EventBus instance.
|
||||
func NewEventBus() *EventBus {
|
||||
return &EventBus{ //nolint:exhaustivestruct
|
||||
subscribers: map[string]eventChannelSlice{},
|
||||
stats: newStats(),
|
||||
}
|
||||
}
|
||||
|
||||
// getSubscribingChannels returns all subscribing channels including wildcard matches.
|
||||
func (eb *EventBus) getSubscribingChannels(topic string) eventChannelSlice {
|
||||
subChannels := eventChannelSlice{}
|
||||
|
||||
for topicName := range eb.subscribers {
|
||||
if topicName == topic || matchWildcard(topicName, topic) {
|
||||
subChannels = append(subChannels, eb.subscribers[topicName]...)
|
||||
}
|
||||
}
|
||||
|
||||
return subChannels
|
||||
}
|
||||
|
||||
// doPublish is publishing events to channels internally.
|
||||
func (eb *EventBus) doPublish(channels eventChannelSlice, evt Event) {
|
||||
eb.mu.RLock()
|
||||
defer eb.mu.RUnlock()
|
||||
|
||||
go func(channels eventChannelSlice, evt Event) {
|
||||
for _, ch := range channels {
|
||||
ch <- evt
|
||||
}
|
||||
}(channels, evt)
|
||||
}
|
||||
|
||||
// Code from https://github.com/minio/minio/blob/master/pkg/wildcard/match.go
|
||||
func matchWildcard(pattern, name string) bool {
|
||||
if pattern == "" {
|
||||
return name == pattern
|
||||
}
|
||||
|
||||
if pattern == "*" {
|
||||
return true
|
||||
}
|
||||
// Does only wildcard '*' match.
|
||||
return deepMatchRune([]rune(name), []rune(pattern), true)
|
||||
}
|
||||
|
||||
// Code from https://github.com/minio/minio/blob/master/pkg/wildcard/match.go
|
||||
func deepMatchRune(str, pattern []rune, simple bool) bool { //nolint:unparam
|
||||
for len(pattern) > 0 {
|
||||
switch pattern[0] {
|
||||
default:
|
||||
if len(str) == 0 || str[0] != pattern[0] {
|
||||
return false
|
||||
}
|
||||
case '*':
|
||||
return deepMatchRune(str, pattern[1:], simple) ||
|
||||
(len(str) > 0 && deepMatchRune(str[1:], pattern, simple))
|
||||
}
|
||||
|
||||
str = str[1:]
|
||||
|
||||
pattern = pattern[1:]
|
||||
}
|
||||
|
||||
return len(str) == 0 && len(pattern) == 0
|
||||
}
|
||||
|
||||
// PublishAsync data to a topic asynchronously
|
||||
// This function returns a bool channel which indicates that all subscribers where called.
|
||||
func (eb *EventBus) PublishAsync(topic string, data Rec) {
|
||||
eb.doPublish(
|
||||
eb.getSubscribingChannels(topic),
|
||||
Event{
|
||||
Data: data,
|
||||
Topic: topic,
|
||||
wg: nil,
|
||||
})
|
||||
|
||||
eb.stats.incPublishedCountByTopic(topic)
|
||||
}
|
||||
|
||||
// PublishAsyncOnce same as PublishAsync but makes sure that topic is only published once.
|
||||
func (eb *EventBus) PublishAsyncOnce(topic string, data Rec) {
|
||||
if eb.stats.GetPublishedCountByTopic(topic) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
eb.PublishAsync(topic, data)
|
||||
}
|
||||
|
||||
// Publish data to a topic and wait for all subscribers to finish
|
||||
// This function creates a waitGroup internally. All subscribers must call Done() function on Event.
|
||||
func (eb *EventBus) Publish(topic string, data Rec) Rec {
|
||||
wg := sync.WaitGroup{}
|
||||
channels := eb.getSubscribingChannels(topic)
|
||||
wg.Add(len(channels))
|
||||
eb.doPublish(
|
||||
channels,
|
||||
Event{
|
||||
Data: data,
|
||||
Topic: topic,
|
||||
wg: &wg,
|
||||
})
|
||||
wg.Wait()
|
||||
|
||||
eb.stats.incPublishedCountByTopic(topic)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// PublishOnce same as Publish but makes sure only published once on topic.
|
||||
func (eb *EventBus) PublishOnce(topic string, data Rec) Rec {
|
||||
if eb.stats.GetPublishedCountByTopic(topic) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return eb.Publish(topic, data)
|
||||
}
|
||||
|
||||
// Subscribe to a topic passing a EventChannel.
|
||||
func (eb *EventBus) Subscribe(topic string) EventChannel {
|
||||
ch := make(EventChannel)
|
||||
eb.SubscribeChannel(topic, ch)
|
||||
|
||||
eb.stats.incSubscriberCountByTopic(topic)
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// SubscribeChannel subscribes to a given Channel.
|
||||
func (eb *EventBus) SubscribeChannel(topic string, ch EventChannel) {
|
||||
eb.mu.Lock()
|
||||
defer eb.mu.Unlock()
|
||||
|
||||
if prev, found := eb.subscribers[topic]; found {
|
||||
eb.subscribers[topic] = append(prev, ch)
|
||||
} else {
|
||||
eb.subscribers[topic] = append([]EventChannel{}, ch)
|
||||
}
|
||||
|
||||
eb.stats.incSubscriberCountByTopic(topic)
|
||||
}
|
||||
|
||||
// SubscribeCallback provides a simple wrapper that allows to directly register CallbackFunc instead of channels.
|
||||
func (eb *EventBus) SubscribeCallback(topic string, callable CallbackFunc) {
|
||||
ch := NewEventChannel()
|
||||
eb.SubscribeChannel(topic, ch)
|
||||
|
||||
go func(callable CallbackFunc) {
|
||||
evt := <-ch
|
||||
callable(evt.Topic, evt.Data)
|
||||
evt.Done()
|
||||
}(callable)
|
||||
|
||||
eb.stats.incSubscriberCountByTopic(topic)
|
||||
}
|
||||
|
||||
// HasSubscribers Check if a topic has subscribers.
|
||||
func (eb *EventBus) HasSubscribers(topic string) bool {
|
||||
return len(eb.getSubscribingChannels(topic)) > 0
|
||||
}
|
||||
|
||||
// Stats returns the stats map.
|
||||
func (eb *EventBus) Stats() *Stats {
|
||||
return eb.stats
|
||||
}
|
||||
|
||||
type TopicStats struct {
|
||||
Name string
|
||||
PublishedCount *SafeCounter
|
||||
SubscriberCount *SafeCounter
|
||||
}
|
||||
|
||||
type topicStatsMap map[string]*TopicStats
|
||||
|
||||
type Stats struct {
|
||||
data topicStatsMap
|
||||
}
|
||||
|
||||
func newStats() *Stats {
|
||||
return &Stats{
|
||||
data: map[string]*TopicStats{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stats) getOrCreateTopicStats(topicName string) *TopicStats {
|
||||
_, ok := s.data[topicName]
|
||||
if !ok {
|
||||
s.data[topicName] = &TopicStats{
|
||||
Name: topicName,
|
||||
PublishedCount: NewSafeCounter(),
|
||||
SubscriberCount: NewSafeCounter(),
|
||||
}
|
||||
}
|
||||
|
||||
return s.data[topicName]
|
||||
}
|
||||
|
||||
func (s *Stats) incSubscriberCountByTopic(topicName string) {
|
||||
s.getOrCreateTopicStats(topicName).SubscriberCount.Inc()
|
||||
}
|
||||
|
||||
func (s *Stats) GetSubscriberCountByTopic(topicName string) int {
|
||||
return s.getOrCreateTopicStats(topicName).SubscriberCount.Value()
|
||||
}
|
||||
|
||||
func (s *Stats) incPublishedCountByTopic(topicName string) {
|
||||
s.getOrCreateTopicStats(topicName).PublishedCount.Inc()
|
||||
}
|
||||
|
||||
func (s *Stats) GetPublishedCountByTopic(topicName string) int {
|
||||
return s.getOrCreateTopicStats(topicName).PublishedCount.Value()
|
||||
}
|
||||
|
||||
func (s *Stats) GetTopicStats() []*TopicStats {
|
||||
var tStatsSlice []*TopicStats
|
||||
for _, tStats := range s.data {
|
||||
tStatsSlice = append(tStatsSlice, tStats)
|
||||
}
|
||||
|
||||
return tStatsSlice
|
||||
}
|
||||
|
||||
func (s *Stats) GetTopicStatsByName(topicName string) *TopicStats {
|
||||
return s.getOrCreateTopicStats(topicName)
|
||||
}
|
||||
|
||||
// SafeCounter is a concurrency safe counter.
|
||||
type SafeCounter struct {
|
||||
v *uint64
|
||||
}
|
||||
|
||||
// NewSafeCounter creates a new counter.
|
||||
func NewSafeCounter() *SafeCounter {
|
||||
return &SafeCounter{
|
||||
v: new(uint64),
|
||||
}
|
||||
}
|
||||
|
||||
// Value returns the current value.
|
||||
func (c *SafeCounter) Value() int {
|
||||
return int(atomic.LoadUint64(c.v))
|
||||
}
|
||||
|
||||
// IncBy increments the counter by given delta.
|
||||
func (c *SafeCounter) IncBy(add uint) {
|
||||
atomic.AddUint64(c.v, uint64(add))
|
||||
}
|
||||
|
||||
// Inc increments the counter by 1.
|
||||
func (c *SafeCounter) Inc() {
|
||||
c.IncBy(1)
|
||||
}
|
||||
|
||||
// DecBy decrements the counter by given delta.
|
||||
func (c *SafeCounter) DecBy(dec uint) {
|
||||
atomic.AddUint64(c.v, ^uint64(dec-1))
|
||||
}
|
||||
|
||||
// Dec decrements the counter by 1.
|
||||
func (c *SafeCounter) Dec() {
|
||||
c.DecBy(1)
|
||||
}
|
||||
102
lcars_v2/experiments.go
Normal file
102
lcars_v2/experiments.go
Normal file
@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ld/server"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func readCrew(server *server.Server) (string, error) {
|
||||
|
||||
content, err := embedded.ReadFile("embed/crew.json")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create a slice to hold the parsed names
|
||||
var crewNames []string
|
||||
|
||||
// Parse the JSON
|
||||
err = json.Unmarshal(content, &crewNames)
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing JSON:", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
const insertStmt = "INSERT INTO crew_member ( rank, name) VALUES (?, ?) ;"
|
||||
insstmt, err := server.StateDB.DB().Prepare(insertStmt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer insstmt.Close()
|
||||
|
||||
// Print the results
|
||||
for _, text := range crewNames {
|
||||
rank, name := splitRank(text)
|
||||
if rank == "" {
|
||||
rank = "ERROR"
|
||||
}
|
||||
// fmt.Printf("%d: rank: %s name: %s\n", i+1, rank, name)
|
||||
_, err = insstmt.Exec(rank, name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
// fmt.Println(string(content))
|
||||
return "", nil
|
||||
|
||||
}
|
||||
|
||||
// splitRank separates the rank (all tokens except the last two) from the crewman's name (last two tokens)
|
||||
func splitRank(fullName string) (rank, name string) {
|
||||
tokens := strings.Fields(fullName)
|
||||
if len(tokens) < 2 {
|
||||
return fullName, "" // fallback if malformed
|
||||
}
|
||||
|
||||
nameTokens := tokens[len(tokens)-2:] // last 2 tokens as name
|
||||
rankTokens := tokens[:len(tokens)-2] // everything else as rank
|
||||
name = strings.Join(nameTokens, " ")
|
||||
rank = strings.Join(rankTokens, " ")
|
||||
|
||||
return rank, name
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Subsystem string `json:"subsystem"`
|
||||
Severity string `json:"severity"`
|
||||
Color string `json:"color"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func readMessages(server *server.Server) error {
|
||||
content, err := embedded.ReadFile("embed/messages.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var messages []Message
|
||||
if err := json.Unmarshal(content, &messages); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const insertStmt = "INSERT INTO ship_messages ( timestamp, subsystem, severity, color, message) VALUES (?,?,?,?,?) ;"
|
||||
insstmt, err := server.StateDB.DB().Prepare(insertStmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer insstmt.Close()
|
||||
// For demonstration, print the parsed messages
|
||||
for _, m := range messages {
|
||||
// fmt.Printf("[%s] %s (%s) - %s\n", m.Timestamp, m.Subsystem, m.Severity, m.Message)
|
||||
_, err = insstmt.Exec(m.Timestamp, m.Subsystem, m.Severity, m.Color, m.Message)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
BIN
lcars_v2/frontend/assets/Antonio-Bold.woff
Normal file
BIN
lcars_v2/frontend/assets/Antonio-Bold.woff
Normal file
Binary file not shown.
BIN
lcars_v2/frontend/assets/Antonio-Bold.woff2
Normal file
BIN
lcars_v2/frontend/assets/Antonio-Bold.woff2
Normal file
Binary file not shown.
BIN
lcars_v2/frontend/assets/Antonio-Regular.woff
Normal file
BIN
lcars_v2/frontend/assets/Antonio-Regular.woff
Normal file
Binary file not shown.
BIN
lcars_v2/frontend/assets/Antonio-Regular.woff2
Normal file
BIN
lcars_v2/frontend/assets/Antonio-Regular.woff2
Normal file
Binary file not shown.
BIN
lcars_v2/frontend/assets/alert.png
Normal file
BIN
lcars_v2/frontend/assets/alert.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
BIN
lcars_v2/frontend/assets/beep1.mp3
Normal file
BIN
lcars_v2/frontend/assets/beep1.mp3
Normal file
Binary file not shown.
BIN
lcars_v2/frontend/assets/beep2.mp3
Normal file
BIN
lcars_v2/frontend/assets/beep2.mp3
Normal file
Binary file not shown.
BIN
lcars_v2/frontend/assets/beep3.mp3
Normal file
BIN
lcars_v2/frontend/assets/beep3.mp3
Normal file
Binary file not shown.
BIN
lcars_v2/frontend/assets/beep4.mp3
Normal file
BIN
lcars_v2/frontend/assets/beep4.mp3
Normal file
Binary file not shown.
3011
lcars_v2/frontend/assets/classic.css
Normal file
3011
lcars_v2/frontend/assets/classic.css
Normal file
File diff suppressed because it is too large
Load Diff
51
lcars_v2/frontend/assets/lcars.js
Normal file
51
lcars_v2/frontend/assets/lcars.js
Normal file
@ -0,0 +1,51 @@
|
||||
document.addEventListener("touchstart", function() {},false);
|
||||
let mybutton = document.getElementById("topBtn");
|
||||
window.onscroll = function() {scrollFunction()};
|
||||
function scrollFunction() {
|
||||
if (document.body.scrollTop > 200 || document.documentElement.scrollTop > 200) {
|
||||
mybutton.style.display = "block";
|
||||
} else {
|
||||
mybutton.style.display = "none";
|
||||
}
|
||||
}
|
||||
function topFunction() {
|
||||
document.body.scrollTop = 0;
|
||||
document.documentElement.scrollTop = 0;
|
||||
}
|
||||
function playSoundAndRedirect(audioId, url) {
|
||||
// var audio = document.getElementById(audioId);
|
||||
// audio.play();
|
||||
|
||||
// audio.onended = function() {
|
||||
// console.log("Audio has ended", url);
|
||||
window.location.href = url;
|
||||
// };
|
||||
}
|
||||
function goToAnchor(anchorId) {
|
||||
window.location.hash = anchorId;
|
||||
}
|
||||
// Accordion drop-down
|
||||
var acc = document.getElementsByClassName("accordion");
|
||||
var i;
|
||||
|
||||
for (i = 0; i < acc.length; i++) {
|
||||
acc[i].addEventListener("click", function() {
|
||||
this.classList.toggle("active");
|
||||
var accordionContent = this.nextElementSibling;
|
||||
if (accordionContent.style.maxHeight){
|
||||
accordionContent.style.maxHeight = null;
|
||||
} else {
|
||||
accordionContent.style.maxHeight = accordionContent.scrollHeight + "px";
|
||||
}
|
||||
});
|
||||
}
|
||||
// LCARS keystroke sound (not to be used with hyperlinks)
|
||||
const LCARSkeystroke = document.getElementById('LCARSkeystroke');
|
||||
const allPlaySoundButtons = document.querySelectorAll('.playSoundButton');
|
||||
allPlaySoundButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
LCARSkeystroke.pause();
|
||||
LCARSkeystroke.currentTime = 0; // Reset to the beginning of the sound
|
||||
LCARSkeystroke.play();
|
||||
});
|
||||
});
|
||||
1885
lcars_v2/frontend/assets/lower-decks-padd.css
Normal file
1885
lcars_v2/frontend/assets/lower-decks-padd.css
Normal file
File diff suppressed because it is too large
Load Diff
1856
lcars_v2/frontend/assets/lower-decks.css
Normal file
1856
lcars_v2/frontend/assets/lower-decks.css
Normal file
File diff suppressed because it is too large
Load Diff
2830
lcars_v2/frontend/assets/nemesis-blue.css
Normal file
2830
lcars_v2/frontend/assets/nemesis-blue.css
Normal file
File diff suppressed because it is too large
Load Diff
231
lcars_v2/frontend/crew/index.html
Normal file
231
lcars_v2/frontend/crew/index.html
Normal file
@ -0,0 +1,231 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Lower Decks PADD</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="format-detection" content="date=no">
|
||||
<link rel="stylesheet" type="text/css" href="../assets/lower-decks-padd.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- <audio id="audio1" src="assets/beep1.mp3" preload="auto"></audio>
|
||||
<audio id="audio2" src="assets/beep2.mp3" preload="auto"></audio>
|
||||
<audio id="audio3" src="assets/beep3.mp3" preload="auto"></audio>
|
||||
<audio id="audio4" src="assets/beep4.mp3" preload="auto"></audio> -->
|
||||
<div class="wrap-all">
|
||||
<div class="wrap">
|
||||
<div class="left-frame-top">
|
||||
<!--
|
||||
*** LCARS PANEL BUTTON ***
|
||||
Replace the hashtag '#' with a real URL (or not) in the following <button> tag. If you do not want a sound effect for this link, replace the <button> element with the following <div> + <a> elements:
|
||||
|
||||
<div class="panel-1">
|
||||
<a href="#">LCARS</a>
|
||||
</div>
|
||||
-->
|
||||
<a href="/" class="panel-1-button">LCARS</a>
|
||||
<!-- <button onclick="playSoundAndRedirect('audio2', '/')" class="panel-1-button">LCARS</button> -->
|
||||
<div class="panel-2">02<span class="hop">-262000</span></div>
|
||||
</div>
|
||||
<div class="right-frame-top">
|
||||
<div class="banner">LCARS 57436.2</div>
|
||||
<div class="data-cascade-button-group">
|
||||
<div class="data-wrapper">
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">47</div>
|
||||
<div class="dc-row-2">31</div>
|
||||
<div class="dc-row-3">28</div>
|
||||
<div class="dc-row-4">94</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">329</div>
|
||||
<div class="dc-row-2 font-night-rain">128</div>
|
||||
<div class="dc-row-3">605</div>
|
||||
<div class="dc-row-4">704</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-night-rain">39725514862</div>
|
||||
<div class="dc-row-2 font-arctic-ice">51320259663</div>
|
||||
<div class="dc-row-3 font-alpha-blue">21857221984</div>
|
||||
<div class="dc-row-4">40372566301</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">56</div>
|
||||
<div class="dc-row-2 font-night-rain">04</div>
|
||||
<div class="dc-row-3 font-night-rain">40</div>
|
||||
<div class="dc-row-4 font-night-rain">35</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">614</div>
|
||||
<div class="dc-row-2 font-arctic-ice">883</div>
|
||||
<div class="dc-row-3 font-alpha-blue">109</div>
|
||||
<div class="dc-row-4">297</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 darkspace darkfont">000</div>
|
||||
<div class="dc-row-2 darkspace font-alpha-blue">13</div>
|
||||
<div class="dc-row-3 darkspace font-arctic-ice">05</div>
|
||||
<div class="dc-row-4 darkspace font-night-rain">25</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">48</div>
|
||||
<div class="dc-row-2 font-night-rain">07</div>
|
||||
<div class="dc-row-3">38</div>
|
||||
<div class="dc-row-4">62</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">416</div>
|
||||
<div class="dc-row-2 font-night-rain">001</div>
|
||||
<div class="dc-row-3">888</div>
|
||||
<div class="dc-row-4">442</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-night-rain">86225514862</div>
|
||||
<div class="dc-row-2 font-arctic-ice">31042009183</div>
|
||||
<div class="dc-row-3 font-alpha-blue">74882306985</div>
|
||||
<div class="dc-row-4">54048523421</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-alpha-blue">10</div>
|
||||
<div class="dc-row-2">80</div>
|
||||
<div class="dc-row-3 font-night-rain">31</div>
|
||||
<div class="dc-row-4 font-alpha-blue">85</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-alpha-blue">87</div>
|
||||
<div class="dc-row-2">71</div>
|
||||
<div class="dc-row-3 font-night-rain">40</div>
|
||||
<div class="dc-row-4 font-night-rain">26</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">98</div>
|
||||
<div class="dc-row-2">63</div>
|
||||
<div class="dc-row-3 font-night-rain">52</div>
|
||||
<div class="dc-row-4 font-alpha-blue">71</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">118</div>
|
||||
<div class="dc-row-2">270</div>
|
||||
<div class="dc-row-3">395</div>
|
||||
<div class="dc-row-4">260</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">8675309</div>
|
||||
<div class="dc-row-2 font-night-rain">7952705</div>
|
||||
<div class="dc-row-3">9282721</div>
|
||||
<div class="dc-row-4">4981518</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 darkspace darkfont">000</div>
|
||||
<div class="dc-row-2 darkspace font-alpha-blue">99</div>
|
||||
<div class="dc-row-3 darkspace font-arctic-ice">10</div>
|
||||
<div class="dc-row-4 darkspace font-night-rain">84</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">65821407321</div>
|
||||
<div class="dc-row-2 font-alpha-blue">54018820533</div>
|
||||
<div class="dc-row-3 font-night-rain">27174523016</div>
|
||||
<div class="dc-row-4">38954062564</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">999</div>
|
||||
<div class="dc-row-2 font-arctic-ice">202</div>
|
||||
<div class="dc-row-3 font-alpha-blue">574</div>
|
||||
<div class="dc-row-4">293</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">3872</div>
|
||||
<div class="dc-row-2 font-night-rain">1105</div>
|
||||
<div class="dc-row-3">1106</div>
|
||||
<div class="dc-row-4 font-alpha-blue">7411</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<!--
|
||||
*** MAIN NAVIGATION BUTTONS ***
|
||||
Replace the hashtag '#' with a real URL (or not).
|
||||
If you don't want sound effects, replace the <button> element with a basic <a> tag shown here in this comment:
|
||||
<a href="#">01</a>
|
||||
<a href="#">02</a>
|
||||
<a href="#">03</a>
|
||||
<a href="#">04</a>
|
||||
-->
|
||||
<button onclick="playSoundAndRedirect('audio2', 'crew.html')">01</button>
|
||||
<button onclick="playSoundAndRedirect('audio2', '#')">02</button>
|
||||
<button onclick="playSoundAndRedirect('audio2', '#')">03</button>
|
||||
<button onclick="playSoundAndRedirect('audio2', '#')">04</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="bar-panel first-bar-panel">
|
||||
<div class="bar-1"> </div>
|
||||
<div class="bar-2"> </div>
|
||||
<div class="bar-3"> </div>
|
||||
<div class="bar-4"> </div>
|
||||
<div class="bar-5"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider">
|
||||
<div class="block-left"> </div>
|
||||
<div class="block-right">
|
||||
<div class="block-row">
|
||||
<div class="bar-11"> </div>
|
||||
<div class="bar-12"> </div>
|
||||
<div class="bar-13"> </div>
|
||||
<div class="bar-14">
|
||||
<div class="blockhead"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="left-frame">
|
||||
<!--
|
||||
** SCROLL TO TOP OF PAGE BUTTON **
|
||||
This button is initially hidden, and is styled like a panel in the sidebar. It appears at the bottom of the page after vertical scrolling. If you don't want the sound effect, replace with this:
|
||||
<button onclick="topFunction()" id="topBtn"><span class="hop">screen</span> top</button>
|
||||
-->
|
||||
<button onclick="topFunction(); playSoundAndRedirect('audio4', '#')" id="topBtn"><span
|
||||
class="hop">screen</span> top</button>
|
||||
<div>
|
||||
<div class="panel-3">03<span class="hop">-111968</span></div>
|
||||
<div class="panel-4">04<span class="hop">-041969</span></div>
|
||||
<div class="panel-5">05<span class="hop">-1701D</span></div>
|
||||
<div class="panel-6">06<span class="hop">-071984</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="panel-7">07<span class="hop">-081940</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-frame">
|
||||
<div class="bar-panel">
|
||||
<div class="bar-6"> </div>
|
||||
<div class="bar-7"> </div>
|
||||
<div class="bar-8"> </div>
|
||||
<div class="bar-9"> </div>
|
||||
<div class="bar-10"> </div>
|
||||
</div>
|
||||
<main>
|
||||
<h1>Crew Sheet</h1>
|
||||
|
||||
</main>
|
||||
<footer>
|
||||
<!-- Your copyright information is only a suggestion and you can choose to delete it. -->
|
||||
Content Copyright © 2025 ld.hedeler.com <br>
|
||||
|
||||
<!-- The following attribution must not be removed: -->
|
||||
LCARS Inspired Website Template by <a href="https://www.thelcars.com">www.TheLCARS.com</a>.
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="assets/lcars.js"></script>
|
||||
<div class="headtrim"> </div>
|
||||
<div class="baseboard"> </div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
232
lcars_v2/frontend/index.html
Normal file
232
lcars_v2/frontend/index.html
Normal file
@ -0,0 +1,232 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Lower Decks PADD</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="format-detection" content="date=no">
|
||||
<link rel="stylesheet" type="text/css" href="../assets/lower-decks-padd.css">
|
||||
<script type="speculationrules">
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- <audio id="audio1" src="assets/beep1.mp3" preload="auto"></audio>
|
||||
<audio id="audio2" src="assets/beep2.mp3" preload="auto"></audio>
|
||||
<audio id="audio3" src="assets/beep3.mp3" preload="auto"></audio>
|
||||
<audio id="audio4" src="assets/beep4.mp3" preload="auto"></audio> -->
|
||||
<div class="wrap-all">
|
||||
<div class="wrap">
|
||||
<div class="left-frame-top">
|
||||
<!--
|
||||
*** LCARS PANEL BUTTON ***
|
||||
Replace the hashtag '#' with a real URL (or not) in the following <button> tag. If you do not want a sound effect for this link, replace the <button> element with the following <div> + <a> elements:
|
||||
|
||||
<div class="panel-1">
|
||||
<a href="#">LCARS</a>
|
||||
</div>
|
||||
-->
|
||||
<button class="panel-1-button">LCARS</button>
|
||||
<div class="panel-2">02<span class="hop">-262000</span></div>
|
||||
</div>
|
||||
<div class="right-frame-top">
|
||||
<div class="banner">LCARS 57436.2</div>
|
||||
<div class="data-cascade-button-group">
|
||||
<div class="data-wrapper">
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">47</div>
|
||||
<div class="dc-row-2">31</div>
|
||||
<div class="dc-row-3">28</div>
|
||||
<div class="dc-row-4">94</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">329</div>
|
||||
<div class="dc-row-2 font-night-rain">128</div>
|
||||
<div class="dc-row-3">605</div>
|
||||
<div class="dc-row-4">704</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-night-rain">39725514862</div>
|
||||
<div class="dc-row-2 font-arctic-ice">51320259663</div>
|
||||
<div class="dc-row-3 font-alpha-blue">21857221984</div>
|
||||
<div class="dc-row-4">40372566301</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">56</div>
|
||||
<div class="dc-row-2 font-night-rain">04</div>
|
||||
<div class="dc-row-3 font-night-rain">40</div>
|
||||
<div class="dc-row-4 font-night-rain">35</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">614</div>
|
||||
<div class="dc-row-2 font-arctic-ice">883</div>
|
||||
<div class="dc-row-3 font-alpha-blue">109</div>
|
||||
<div class="dc-row-4">297</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 darkspace darkfont">000</div>
|
||||
<div class="dc-row-2 darkspace font-alpha-blue">13</div>
|
||||
<div class="dc-row-3 darkspace font-arctic-ice">05</div>
|
||||
<div class="dc-row-4 darkspace font-night-rain">25</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">48</div>
|
||||
<div class="dc-row-2 font-night-rain">07</div>
|
||||
<div class="dc-row-3">38</div>
|
||||
<div class="dc-row-4">62</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">416</div>
|
||||
<div class="dc-row-2 font-night-rain">001</div>
|
||||
<div class="dc-row-3">888</div>
|
||||
<div class="dc-row-4">442</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-night-rain">86225514862</div>
|
||||
<div class="dc-row-2 font-arctic-ice">31042009183</div>
|
||||
<div class="dc-row-3 font-alpha-blue">74882306985</div>
|
||||
<div class="dc-row-4">54048523421</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-alpha-blue">10</div>
|
||||
<div class="dc-row-2">80</div>
|
||||
<div class="dc-row-3 font-night-rain">31</div>
|
||||
<div class="dc-row-4 font-alpha-blue">85</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-alpha-blue">87</div>
|
||||
<div class="dc-row-2">71</div>
|
||||
<div class="dc-row-3 font-night-rain">40</div>
|
||||
<div class="dc-row-4 font-night-rain">26</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">98</div>
|
||||
<div class="dc-row-2">63</div>
|
||||
<div class="dc-row-3 font-night-rain">52</div>
|
||||
<div class="dc-row-4 font-alpha-blue">71</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">118</div>
|
||||
<div class="dc-row-2">270</div>
|
||||
<div class="dc-row-3">395</div>
|
||||
<div class="dc-row-4">260</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">8675309</div>
|
||||
<div class="dc-row-2 font-night-rain">7952705</div>
|
||||
<div class="dc-row-3">9282721</div>
|
||||
<div class="dc-row-4">4981518</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 darkspace darkfont">000</div>
|
||||
<div class="dc-row-2 darkspace font-alpha-blue">99</div>
|
||||
<div class="dc-row-3 darkspace font-arctic-ice">10</div>
|
||||
<div class="dc-row-4 darkspace font-night-rain">84</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">65821407321</div>
|
||||
<div class="dc-row-2 font-alpha-blue">54018820533</div>
|
||||
<div class="dc-row-3 font-night-rain">27174523016</div>
|
||||
<div class="dc-row-4">38954062564</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1 font-arctic-ice">999</div>
|
||||
<div class="dc-row-2 font-arctic-ice">202</div>
|
||||
<div class="dc-row-3 font-alpha-blue">574</div>
|
||||
<div class="dc-row-4">293</div>
|
||||
</div>
|
||||
<div class="data-column">
|
||||
<div class="dc-row-1">3872</div>
|
||||
<div class="dc-row-2 font-night-rain">1105</div>
|
||||
<div class="dc-row-3">1106</div>
|
||||
<div class="dc-row-4 font-alpha-blue">7411</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<!--
|
||||
*** MAIN NAVIGATION BUTTONS ***
|
||||
Replace the hashtag '#' with a real URL (or not).
|
||||
If you don't want sound effects, replace the <button> element with a basic <a> tag shown here in this comment:
|
||||
<a href="#">01</a>
|
||||
<a href="#">02</a>
|
||||
<a href="#">03</a>
|
||||
<a href="#">04</a>
|
||||
-->
|
||||
<a href="crew">01</a>
|
||||
<button onclick="playSoundAndRedirect('audio2', '#')">02</button>
|
||||
<button onclick="playSoundAndRedirect('audio2', '#')">03</button>
|
||||
<button onclick="playSoundAndRedirect('audio2', '#')">04</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="bar-panel first-bar-panel">
|
||||
<div class="bar-1"> </div>
|
||||
<div class="bar-2"> </div>
|
||||
<div class="bar-3"> </div>
|
||||
<div class="bar-4"> </div>
|
||||
<div class="bar-5"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider">
|
||||
<div class="block-left"> </div>
|
||||
<div class="block-right">
|
||||
<div class="block-row">
|
||||
<div class="bar-11"> </div>
|
||||
<div class="bar-12"> </div>
|
||||
<div class="bar-13"> </div>
|
||||
<div class="bar-14">
|
||||
<div class="blockhead"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="left-frame">
|
||||
<!--
|
||||
** SCROLL TO TOP OF PAGE BUTTON **
|
||||
This button is initially hidden, and is styled like a panel in the sidebar. It appears at the bottom of the page after vertical scrolling. If you don't want the sound effect, replace with this:
|
||||
<button onclick="topFunction()" id="topBtn"><span class="hop">screen</span> top</button>
|
||||
-->
|
||||
<button onclick="topFunction(); playSoundAndRedirect('audio4', '#')" id="topBtn"><span
|
||||
class="hop">screen</span> top</button>
|
||||
<div>
|
||||
<div class="panel-3">03<span class="hop">-111968</span></div>
|
||||
<div class="panel-4">04<span class="hop">-041969</span></div>
|
||||
<div class="panel-5">05<span class="hop">-1701D</span></div>
|
||||
<div class="panel-6">06<span class="hop">-071984</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="panel-7">07<span class="hop">-081940</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-frame">
|
||||
<div class="bar-panel">
|
||||
<div class="bar-6"> </div>
|
||||
<div class="bar-7"> </div>
|
||||
<div class="bar-8"> </div>
|
||||
<div class="bar-9"> </div>
|
||||
<div class="bar-10"> </div>
|
||||
</div>
|
||||
<main>
|
||||
<h1>Lower Decks PADD</h1>
|
||||
|
||||
</main>
|
||||
<footer>
|
||||
<!-- Your copyright information is only a suggestion and you can choose to delete it. -->
|
||||
Content Copyright © 2025 ld.hedeler.com <br>
|
||||
|
||||
<!-- The following attribution must not be removed: -->
|
||||
LCARS Inspired Website Template by <a href="https://www.thelcars.com">www.TheLCARS.com</a>.
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="assets/lcars.js"></script>
|
||||
<div class="headtrim"> </div>
|
||||
<div class="baseboard"> </div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
39
lcars_v2/frontend/tinyserver.go
Normal file
39
lcars_v2/frontend/tinyserver.go
Normal file
@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Folder to serve
|
||||
dir := "."
|
||||
|
||||
// File server handler
|
||||
fs := http.FileServer(http.Dir(dir))
|
||||
|
||||
// Wrap the file server to add caching headers
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ext := strings.ToLower(filepath.Ext(r.URL.Path))
|
||||
|
||||
// Set long caching for static assets
|
||||
switch ext {
|
||||
case ".css", ".js", ".woff", ".woff2", ".ttf", ".eot", ".svg", ".png", ".jpg", ".jpeg", ".gif":
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||
default:
|
||||
// Optional: short caching for HTML so you always get latest page
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
}
|
||||
|
||||
fs.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
addr := ":8080"
|
||||
log.Printf("Serving %s on http://localhost%s\n", dir, addr)
|
||||
err := http.ListenAndServe(addr, handler)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
18
lcars_v2/go.mod
Normal file
18
lcars_v2/go.mod
Normal file
@ -0,0 +1,18 @@
|
||||
module ld
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require modernc.org/sqlite v1.39.0
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
49
lcars_v2/go.sum
Normal file
49
lcars_v2/go.sum
Normal file
@ -0,0 +1,49 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
|
||||
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
94
lcars_v2/interval/interval.go
Normal file
94
lcars_v2/interval/interval.go
Normal file
@ -0,0 +1,94 @@
|
||||
package interval
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MutexMap[K comparable, V any] struct {
|
||||
mutex sync.Mutex
|
||||
m map[K]V
|
||||
}
|
||||
|
||||
func (m *MutexMap[K, V]) Get(key K) (V, error) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
v, ok := m.m[key]
|
||||
if !ok {
|
||||
return v, errors.New("unknown key")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (m *MutexMap[K, V]) Set(key K, value V) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
m.m[key] = value
|
||||
}
|
||||
|
||||
func (m *MutexMap[K, V]) Delete(key K) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
delete(m.m, key)
|
||||
}
|
||||
|
||||
var stopChannels = MutexMap[int, chan bool]{
|
||||
m: make(map[int]chan bool),
|
||||
}
|
||||
|
||||
// SetInterval schedules a repeating task to be executed at a specified interval.
|
||||
func SetInterval(f func(), milliseconds int) (id int) {
|
||||
for {
|
||||
id = rand.Int()
|
||||
if _, err := stopChannels.Get(id); err == nil {
|
||||
continue // ID collision, keep looking for another unique random value
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
stop := make(chan bool)
|
||||
stopChannels.Set(id, stop)
|
||||
|
||||
ticker := time.NewTicker(time.Duration(milliseconds) * time.Millisecond)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
f()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ClearInterval stops a scheduled interval identified by the specified interval ID.
|
||||
func ClearInterval(id int) error {
|
||||
stop, err := stopChannels.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stop <- true
|
||||
stopChannels.Delete(id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTimeout schedules a one-time task to be executed after a specified interval.
|
||||
func SetTimeout(f func(), milliseconds int) {
|
||||
timer := time.NewTimer(time.Duration(milliseconds) * time.Millisecond)
|
||||
go func() {
|
||||
<-timer.C
|
||||
timer.Stop()
|
||||
f()
|
||||
}()
|
||||
}
|
||||
217
lcars_v2/main.go
Normal file
217
lcars_v2/main.go
Normal file
@ -0,0 +1,217 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"ld/eventbus"
|
||||
"ld/interval"
|
||||
"ld/server"
|
||||
"ld/sqlite"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const (
|
||||
exitCodeErr = 1
|
||||
exitCodeInterrupt = 2
|
||||
)
|
||||
|
||||
var AppRoot = "./" // path for supporting files that sit in app root folder in production
|
||||
|
||||
//go:embed frontend/*
|
||||
var frontend embed.FS
|
||||
|
||||
//go:embed embed
|
||||
var embedded embed.FS
|
||||
|
||||
// main specific variables
|
||||
var ExecutableName string
|
||||
|
||||
func main() {
|
||||
fmt.Println("Here")
|
||||
err := run()
|
||||
if err != nil {
|
||||
os.Exit(exitCodeErr)
|
||||
}
|
||||
ExecutableName, err = getExecutableName()
|
||||
if err != nil {
|
||||
os.Exit(exitCodeErr)
|
||||
}
|
||||
|
||||
// readCrew()
|
||||
|
||||
// readMessages()
|
||||
ebus := eventbus.NewEventBus()
|
||||
|
||||
interval.SetInterval(func() {
|
||||
fmt.Println("------------------")
|
||||
}, 10)
|
||||
|
||||
runEventbus(ebus)
|
||||
|
||||
run()
|
||||
|
||||
}
|
||||
|
||||
func runEventbus(ebus *eventbus.EventBus) {
|
||||
|
||||
// Create a new instance
|
||||
eventChannel := eventbus.NewEventChannel()
|
||||
|
||||
// Subscribe to "foo:baz" - or use a wildcard like "foo:*"
|
||||
ebus.SubscribeChannel("foo:baz", eventChannel)
|
||||
ebus.SubscribeChannel("pups-klo", eventChannel)
|
||||
ebus.SubscribeChannel("ömme*", eventChannel)
|
||||
|
||||
eventChannelTopic := ebus.Subscribe("ömmels")
|
||||
|
||||
// Subscribe with existing channel use
|
||||
// eventbus.SubscribeChannel("foo:*", eventChannel)
|
||||
|
||||
// Wait for the incoming event on the channel
|
||||
go func() {
|
||||
|
||||
for evt := range eventChannel {
|
||||
fmt.Println("FIRST", evt.Topic, evt.Data)
|
||||
evt.Done()
|
||||
}
|
||||
|
||||
}()
|
||||
go func() {
|
||||
|
||||
for evt := range eventChannel {
|
||||
fmt.Println("SECOND", evt.Topic, evt.Data)
|
||||
evt.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
|
||||
for evt := range eventChannelTopic {
|
||||
fmt.Println("ÖMMELS", evt.Topic, evt.Data)
|
||||
evt.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
|
||||
ebus.Publish("foo:baz", eventbus.Rec{"value": i})
|
||||
ebus.Publish("pups-klo", eventbus.Rec{"value": i})
|
||||
ebus.Publish("pups-klo", eventbus.Rec{"value": i})
|
||||
ebus.Publish("ömmels", eventbus.Rec{"value": i})
|
||||
ebus.Publish("ömmels", eventbus.Rec{"value": i * 10})
|
||||
ebus.Publish("ömmels", eventbus.Rec{"value": i * 100})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func run() error {
|
||||
|
||||
// setting up logging to file
|
||||
logFileName := AppRoot + ExecutableName + ".log"
|
||||
logFile, err := os.OpenFile(logFileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
log.Printf("error opening file: %v", err)
|
||||
os.Exit(exitCodeErr)
|
||||
}
|
||||
defer logFile.Close()
|
||||
log.SetOutput(logFile)
|
||||
|
||||
stateDB, err := createStateDB(true)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create internal StateDB: %v", err)
|
||||
}
|
||||
|
||||
// setting up the server
|
||||
server, err := server.New(
|
||||
logFileName,
|
||||
stateDB,
|
||||
embedded,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("server-app not created - error: %v", err)
|
||||
}
|
||||
|
||||
// tasks.SetupTasks(server)
|
||||
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
fmt.Printf("server not started - error: %v \n", err)
|
||||
log.Fatalf("server not started - error: %v", err)
|
||||
}
|
||||
|
||||
readCrew(server)
|
||||
readMessages(server)
|
||||
return nil
|
||||
|
||||
// listen for os shutdown events, report them into log file and exit application
|
||||
chanOS := make(chan os.Signal, 2)
|
||||
signal.Notify(chanOS, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-chanOS
|
||||
log.Println("shutting down request signal received")
|
||||
server.Close()
|
||||
os.Exit(exitCodeInterrupt)
|
||||
}()
|
||||
|
||||
// setting up the routes, hooking up API endpoints with backend functions
|
||||
// routes.SetupRoutes(server)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// some internal functions
|
||||
|
||||
func getExecutableName() (string, error) {
|
||||
name, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name = filepath.Base(name)
|
||||
return strings.TrimSuffix(name, filepath.Ext(name)), nil
|
||||
}
|
||||
|
||||
func createStateDB(StateDBDelete bool) (*sqlite.Database, error) {
|
||||
|
||||
// fileName := fmt.Sprintf("state-%s.db", ulid.Make())
|
||||
|
||||
fileName := "state.db"
|
||||
|
||||
if StateDBDelete {
|
||||
_, err := os.Stat(fileName)
|
||||
if err == nil {
|
||||
err := os.Remove(fileName)
|
||||
if err != nil {
|
||||
log.Fatal("error deleting statedb-file:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db, err := sqlite.New(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = db.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query, err := embedded.ReadFile("embed/create_state_db.sql")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = db.DB().Exec(string(query))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
2
lcars_v2/notes.txt
Normal file
2
lcars_v2/notes.txt
Normal file
@ -0,0 +1,2 @@
|
||||
<a target="_blank" href="https://icons8.com/icon/21039/star-trek">Nächste-Generation Abzeichen</a> Icon von <a target="_blank" href="https://icons8.com">Icons8</a>
|
||||
<a href="https://www.flaticon.com/free-icons/geek" title="geek icons">Geek icons created by Pixel perfect - Flaticon</a>
|
||||
105
lcars_v2/server/server.go
Normal file
105
lcars_v2/server/server.go
Normal file
@ -0,0 +1,105 @@
|
||||
// Copyright 2024 codeM GmbH
|
||||
// Author: Thomas Hedeler
|
||||
package server
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"ld/eventbus"
|
||||
"ld/interval"
|
||||
"ld/sqlite"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
SQLiteVersion string
|
||||
ServerInfo map[string]any
|
||||
StateDB *sqlite.Database
|
||||
Embedded embed.FS
|
||||
LogFileName string
|
||||
TokenDuration int // TODO einbauen
|
||||
Secret []byte
|
||||
Header string
|
||||
intervalID int
|
||||
Ebus *eventbus.EventBus
|
||||
Tasks map[string]TaskFunc
|
||||
}
|
||||
|
||||
func New(
|
||||
logfilename string,
|
||||
StateDB *sqlite.Database,
|
||||
embedded embed.FS,
|
||||
|
||||
) (*Server, error) {
|
||||
|
||||
// creating the server
|
||||
return &Server{
|
||||
LogFileName: logfilename,
|
||||
StateDB: StateDB,
|
||||
Embedded: embedded,
|
||||
Tasks: make(map[string]func(s *Server) error),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
|
||||
// query, err := s.Embedded.ReadFile("embed/win/server_info.sql")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// res, err := s.StundenDB.ReadRecords(string(query))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// s.ServerInfo = res[0]
|
||||
|
||||
// err = inits.LoadLogins(s.StundenDB, s.StateDB)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// err = inits.LoadTasks(s.StundenDB, s.StateDB)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // start the task engine
|
||||
// if s.Production {
|
||||
// s.intervalID = interval.SetInterval(s.interval, 60000) // check for executable tasks every 60 seconds
|
||||
// } else {
|
||||
// s.intervalID = interval.SetInterval(s.interval, 30000) // check for executable tasks every 30 seconds
|
||||
// }
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Close() error {
|
||||
|
||||
// stop the task engine
|
||||
err := interval.ClearInterval(s.intervalID)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
|
||||
err = s.StateDB.Close()
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// func to dispatch routes to all parts of the application:
|
||||
// they receive references to the server and the current fiber context via closures
|
||||
// this way all functions have access to server properties and can handle the
|
||||
// incoming requests themselves.
|
||||
|
||||
// type HandlerFunc = func(s *Server, c *fiber.Ctx) error
|
||||
|
||||
// func (s *Server) Handler(handler HandlerFunc) func(c *fiber.Ctx) error {
|
||||
// return func(c *fiber.Ctx) error {
|
||||
// return handler(s, c)
|
||||
// }
|
||||
// }
|
||||
|
||||
// signature for internal tasks
|
||||
type TaskFunc = func(s *Server) error
|
||||
118
lcars_v2/server/taskengine.go
Normal file
118
lcars_v2/server/taskengine.go
Normal file
@ -0,0 +1,118 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// this function schedules the tasks and will be called periodically, see server.Start()
|
||||
func (s *Server) interval() {
|
||||
|
||||
// read scheduled task list from stateDB
|
||||
// check for next executable task:
|
||||
// - if there is one or more tasks ready for execution then select one of them.
|
||||
// - if there is a selected task, update its next execution field and execute the task
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// fmt.Println("Recovered from panic in worker:", r)
|
||||
log.Printf("recovered from panic in taskengine: %v ", r)
|
||||
}
|
||||
}()
|
||||
|
||||
tasks, err := s.StateDB.ReadRecords("SELECT * FROM tasks ORDER by next_execution limit 1;")
|
||||
|
||||
if err != nil {
|
||||
log.Printf("error in taskengine: %s ", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(tasks) < 1 {
|
||||
log.Printf("error in taskengine: %s ", "found no task for execution")
|
||||
return
|
||||
}
|
||||
|
||||
task := tasks[0] // pick the one task with the smallest next execution time, see previous sql statement
|
||||
|
||||
task_name, haveTask := task["task_name"].(string)
|
||||
if !haveTask {
|
||||
log.Printf("error in taskengine: task %s is of wrong type", task["task_name"])
|
||||
return
|
||||
}
|
||||
|
||||
nextExecution := task["next_execution"].(int64)
|
||||
startTime := task["start_time"].(int64)
|
||||
execInterval := task["interval"].(int64)
|
||||
nowSeconds := time.Now().Unix()
|
||||
|
||||
if nowSeconds < nextExecution { // task execution is not yet due
|
||||
return
|
||||
}
|
||||
|
||||
// calculate next execution time
|
||||
for nextExecution = startTime; nowSeconds > nextExecution; nextExecution += execInterval {
|
||||
// add as many intervals to the starttime until the next execution lies in the future
|
||||
}
|
||||
|
||||
task["start_time"] = task["next_execution"]
|
||||
task["next_execution"] = nextExecution
|
||||
|
||||
/*
|
||||
no_executions INTEGER, -- how often executed
|
||||
duration INTEGER, -- duration of the last exec in ms
|
||||
no_errors INTEGER, -- error count
|
||||
last_error_text TEXT,
|
||||
|
||||
*/
|
||||
|
||||
if count, ok := task["no_executions"].(int64); ok {
|
||||
task["no_executions"] = count + 1
|
||||
}
|
||||
|
||||
// update next_execution in state database
|
||||
_, err = s.StateDB.UpsertRecord("tasks", "task_id", task)
|
||||
if err != nil {
|
||||
log.Printf("error in taskengine: cannot update task record - before execution %s ", err)
|
||||
return
|
||||
}
|
||||
|
||||
task_func, haveTask := s.Tasks[task_name] // select the function with the matching name
|
||||
|
||||
if !haveTask {
|
||||
log.Printf("error in taskengine: task %s is not defined", task_name)
|
||||
}
|
||||
|
||||
if haveTask {
|
||||
|
||||
start := time.Now()
|
||||
// if !s.Production {
|
||||
// fmt.Println("Taskengine: executing task:", task_name, start)
|
||||
// }
|
||||
err = task_func(s) // finally execute the task; attention: a task that panics will kill the server!
|
||||
|
||||
task["duration"] = int(time.Since(start).Milliseconds())
|
||||
|
||||
if err != nil {
|
||||
log.Printf("taskengine: execution task: %s failed with error: %s ", task_name, err)
|
||||
task["last_error_text"] = err.Error()
|
||||
if count, ok := task["no_errors"].(int64); ok {
|
||||
task["no_errors"] = count + 1
|
||||
}
|
||||
// if !s.Production {
|
||||
// fmt.Println("Taskengine: failed task:", task_name, err)
|
||||
// }
|
||||
} else {
|
||||
// if !s.Production {
|
||||
// fmt.Println("Taskengine: successfully completed task:", task_name, time.Now())
|
||||
// }
|
||||
}
|
||||
|
||||
_, err = s.StateDB.UpsertRecord("tasks", "task_id", task)
|
||||
if err != nil {
|
||||
log.Printf("error in taskengine: cannot update task record - after execution %s ", err)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
428
lcars_v2/sqlite/database.go
Normal file
428
lcars_v2/sqlite/database.go
Normal file
@ -0,0 +1,428 @@
|
||||
package sqlite // name the package as you see fit
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// This is the data type to exchange data with the database
|
||||
type Record = map[string]any
|
||||
|
||||
type Database struct {
|
||||
databaseName string
|
||||
database *sql.DB
|
||||
}
|
||||
|
||||
type Transaction struct {
|
||||
tx *sql.Tx
|
||||
err error
|
||||
}
|
||||
|
||||
type Action func(tx *sql.Tx) error
|
||||
|
||||
func New(DBName string) (*Database, error) {
|
||||
return &Database{databaseName: DBName}, nil
|
||||
}
|
||||
|
||||
func (d *Database) Close() error {
|
||||
return d.database.Close()
|
||||
}
|
||||
|
||||
// provides access to the internal database object
|
||||
func (d *Database) DB() *sql.DB {
|
||||
return d.database
|
||||
}
|
||||
|
||||
func (d *Database) Name() string {
|
||||
return d.databaseName
|
||||
}
|
||||
|
||||
func (d *Database) Open() (err error) {
|
||||
d.database, err = openSqliteDB(d.databaseName)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) OpenInMemory() (err error) {
|
||||
d.database, err = sql.Open("sqlite", ":memory:")
|
||||
return err
|
||||
}
|
||||
|
||||
func openSqliteDB(databasefilename string) (*sql.DB, error) {
|
||||
|
||||
_, err := os.Stat(databasefilename)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return createDB(databasefilename)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sql.Open("sqlite", databasefilename)
|
||||
|
||||
}
|
||||
|
||||
func createDB(dbfileName string) (*sql.DB, error) {
|
||||
|
||||
query := `
|
||||
PRAGMA page_size = 4096;
|
||||
PRAGMA synchronous = off;
|
||||
PRAGMA foreign_keys = off;
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA user_version = 1;
|
||||
`
|
||||
db, err := sql.Open("sqlite", dbfileName)
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
}
|
||||
_, err = db.Exec(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func (d *Database) TableList() (result []Record, err error) {
|
||||
return d.ReadRecords("select name from sqlite_master where type='table';")
|
||||
}
|
||||
|
||||
func (d *Database) ReadTable(tablename string) (result []Record, err error) {
|
||||
|
||||
return d.ReadRecords(fmt.Sprintf("select * from '%s';", tablename))
|
||||
}
|
||||
|
||||
func (d *Database) ReadRecords(query string, args ...any) (result []Record, err error) {
|
||||
|
||||
rows, err := d.DB().Query(query, args...)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return Rows2records(rows)
|
||||
|
||||
}
|
||||
|
||||
func (d *Database) GetRecord(tablename string, idfield string, key any) (result Record, err error) {
|
||||
|
||||
query := fmt.Sprintf("select * from %s where %s = ?;", tablename, idfield)
|
||||
res, err := d.DB().Query(query, key)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer res.Close()
|
||||
return Rows2record(res)
|
||||
|
||||
}
|
||||
|
||||
func (d *Database) UpsertRecord(tablename string, idfield string, record Record) (result Record, err error) {
|
||||
|
||||
return upsert(d.DB(), tablename, idfield, record)
|
||||
|
||||
}
|
||||
|
||||
func (d *Database) DeleteRecord(tablename string, idfield string, id any) (err error) {
|
||||
|
||||
return deleteRecord(d.DB(), tablename, idfield, id)
|
||||
|
||||
}
|
||||
|
||||
// *sql.DB and *sql.Tx both have a method named 'Query',
|
||||
// this way they can both be passed into upsert and deleteRecord function
|
||||
type iquery interface {
|
||||
Query(query string, args ...any) (*sql.Rows, error)
|
||||
}
|
||||
|
||||
func upsert(t iquery, tablename string, idfield string, record Record) (result Record, err error) {
|
||||
|
||||
fields := []string{}
|
||||
data := []any{}
|
||||
for k, v := range record {
|
||||
fields = append(fields, k)
|
||||
data = append(data, v)
|
||||
}
|
||||
query, err := buildUpsertCommand(tablename, idfield, fields)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
res, err := t.Query(query, data...) // res contains the full record - see SQLite: RETURNING *
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer res.Close()
|
||||
return Rows2record(res)
|
||||
|
||||
}
|
||||
|
||||
func deleteRecord(t iquery, tablename string, idfield string, id any) (err error) {
|
||||
|
||||
query := fmt.Sprintf("DELETE FROM \"%s\" WHERE \"%s\" = ?;", tablename, idfield)
|
||||
_, err = t.Query(query, id)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func buildUpsertCommand(tablename string, idfield string, fields []string) (string, error) {
|
||||
var sb strings.Builder
|
||||
sb.Grow(256 + len(fields)*20) // rough preallocation
|
||||
|
||||
// INSERT INTO
|
||||
sb.WriteString(`INSERT INTO "`)
|
||||
sb.WriteString(tablename)
|
||||
sb.WriteString(`"(`)
|
||||
for i, f := range fields {
|
||||
sb.WriteString(` "`)
|
||||
sb.WriteString(f)
|
||||
sb.WriteByte('"')
|
||||
if i < len(fields)-1 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
}
|
||||
sb.WriteString(")\n\tVALUES(")
|
||||
|
||||
// VALUES
|
||||
for i := 0; i < len(fields); i++ {
|
||||
sb.WriteString(" ?")
|
||||
sb.Write(strconv.AppendInt(nil, int64(i+1), 10))
|
||||
if i < len(fields)-1 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
}
|
||||
sb.WriteString(")\n\tON CONFLICT(\"")
|
||||
sb.WriteString(tablename)
|
||||
sb.WriteString(`"."`)
|
||||
sb.WriteString(idfield)
|
||||
sb.WriteString("\")\n\tDO UPDATE SET ")
|
||||
|
||||
// UPDATE SET
|
||||
for i, f := range fields {
|
||||
sb.WriteByte('"')
|
||||
sb.WriteString(f)
|
||||
sb.WriteString(`"= ?`)
|
||||
sb.Write(strconv.AppendInt(nil, int64(i+1), 10))
|
||||
if i < len(fields)-1 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n\tRETURNING *;")
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// func buildUpsertCommand(tablename string, idfield string, fields []string) (result string, err error) {
|
||||
|
||||
// pname := map[string]string{} // assign correct index for parameter name
|
||||
// // parameter position, starts at 1 in sql! So it needs to be calculated by function pname inside template
|
||||
|
||||
// for i, k := range fields {
|
||||
// pname[k] = strconv.Itoa(i + 1)
|
||||
// }
|
||||
// funcMap := template.FuncMap{
|
||||
// "pname": func(fieldname string) string {
|
||||
// return pname[fieldname]
|
||||
// },
|
||||
// }
|
||||
// tableDef := struct {
|
||||
// Tablename string
|
||||
// KeyField string
|
||||
// LastField int
|
||||
// FieldNames []string
|
||||
// }{
|
||||
// Tablename: tablename,
|
||||
// KeyField: idfield,
|
||||
// LastField: len(fields) - 1,
|
||||
// FieldNames: fields,
|
||||
// }
|
||||
// var templString = `{{$last := .LastField}}INSERT INTO "{{ .Tablename }}"({{ range $i,$el := .FieldNames }} "{{$el}}"{{if ne $i $last}},{{end}}{{end}})
|
||||
// VALUES({{ range $i,$el := .FieldNames }} ?{{pname $el}}{{if ne $i $last}},{{end}}{{end}})
|
||||
// ON CONFLICT("{{ .Tablename }}"."{{.KeyField}}")
|
||||
// DO UPDATE SET {{ range $i,$el := .FieldNames }}"{{$el}}"= ?{{pname $el}}{{if ne $i $last}},{{end}}{{end}}
|
||||
// RETURNING *;`
|
||||
|
||||
// dbTempl, err := template.New("upsertDB").Funcs(funcMap).Parse(templString)
|
||||
// if err != nil {
|
||||
// return result, err
|
||||
// }
|
||||
// var templBytes bytes.Buffer
|
||||
// err = dbTempl.Execute(&templBytes, tableDef)
|
||||
// if err != nil {
|
||||
// return result, err
|
||||
// }
|
||||
// return templBytes.String(), nil
|
||||
// }
|
||||
|
||||
func Rows2record(rows *sql.Rows) (Record, error) {
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values := make([]any, len(columns))
|
||||
valuePtrs := make([]any, len(columns))
|
||||
for i := range values {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
result := Record{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(valuePtrs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, col := range columns {
|
||||
result[col] = values[i]
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil, errors.New("no rows found")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func Rows2records(rows *sql.Rows) ([]Record, error) {
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recLength := len(columns)
|
||||
results := []Record{}
|
||||
for rows.Next() {
|
||||
values := make([]any, recLength)
|
||||
valuePtrs := make([]any, recLength)
|
||||
for i := range values {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
record := Record{}
|
||||
if err := rows.Scan(valuePtrs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, col := range columns {
|
||||
record[col] = values[i]
|
||||
}
|
||||
results = append(results, record)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
return nil, errors.New("no rows found")
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (d *Database) Version() (string, error) {
|
||||
result := ""
|
||||
sqliteversion, err := d.ReadRecords("SELECT sqlite_version();")
|
||||
if len(sqliteversion) == 1 {
|
||||
result = sqliteversion[0]["sqlite_version()"].(string)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (d *Database) UserVersion() (int64, error) {
|
||||
|
||||
var result int64
|
||||
userversion, err := d.ReadRecords("PRAGMA user_version;")
|
||||
if len(userversion) == 1 {
|
||||
result = userversion[0]["user_version"].(int64)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (d *Database) Begin() *Transaction {
|
||||
tx, err := d.database.Begin()
|
||||
return &Transaction{tx, err}
|
||||
}
|
||||
|
||||
func (t *Transaction) Next(action Action) *Transaction {
|
||||
if t.err != nil {
|
||||
return t
|
||||
}
|
||||
t.err = action(t.tx)
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Transaction) End() error {
|
||||
|
||||
if t.err != nil {
|
||||
err := t.tx.Rollback()
|
||||
if err != nil {
|
||||
t.err = errors.Join(t.err, err)
|
||||
}
|
||||
return t.err
|
||||
}
|
||||
t.err = t.tx.Commit()
|
||||
return t.err
|
||||
}
|
||||
|
||||
func (t *Transaction) GetRecord(tablename string, idfield string, key any, output Record) *Transaction {
|
||||
|
||||
if t.err != nil {
|
||||
return t
|
||||
}
|
||||
query := fmt.Sprintf("select * from %s where %s = ?;", tablename, idfield)
|
||||
res, err := t.tx.Query(query, key)
|
||||
if err != nil {
|
||||
t.err = err
|
||||
return t
|
||||
}
|
||||
defer res.Close()
|
||||
result, err := Rows2record(res)
|
||||
if err != nil {
|
||||
t.err = err
|
||||
return t
|
||||
}
|
||||
for k := range output {
|
||||
delete(output, k)
|
||||
}
|
||||
for k, v := range result {
|
||||
output[k] = v
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Transaction) UpsertRecord(tablename string, idfield string, record Record, output Record) *Transaction {
|
||||
|
||||
if t.err != nil {
|
||||
return t
|
||||
}
|
||||
result, err := upsert(t.tx, tablename, idfield, record)
|
||||
if err != nil {
|
||||
t.err = err
|
||||
return t
|
||||
}
|
||||
for k := range output {
|
||||
delete(output, k)
|
||||
}
|
||||
for k, v := range result {
|
||||
output[k] = v
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Transaction) DeleteRecord(tablename string, idfield string, id any) *Transaction {
|
||||
|
||||
if t.err != nil {
|
||||
return t
|
||||
}
|
||||
err := deleteRecord(t.tx, tablename, idfield, id)
|
||||
if err != nil {
|
||||
t.err = err
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// returns a value of the provided type, if the field exist and if it can be cast into the provided type parameter
|
||||
func Value[T any](rec Record, field string) (value T, ok bool) {
|
||||
var v any
|
||||
if v, ok = rec[field]; ok {
|
||||
value, ok = v.(T)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// don't report an error if there are simply just 'no rows found'
|
||||
func NoRowsOk(recs []Record, err error) ([]Record, error) {
|
||||
if err != nil && err.Error() != "no rows found" {
|
||||
return recs, err
|
||||
}
|
||||
return recs, nil
|
||||
}
|
||||
196
sqlite/countries.csv
Normal file
196
sqlite/countries.csv
Normal file
@ -0,0 +1,196 @@
|
||||
german_name,population,capital,timezones
|
||||
Afghanistan,40218234,Kabul,"[""UTC+04:30""]"
|
||||
Albanien,2837743,Tirana,"[""UTC+01:00""]"
|
||||
Algerien,44700000,Algiers,"[""UTC+01:00""]"
|
||||
Andorra,77265,"Andorra la Vella","[""UTC+01:00""]"
|
||||
Angola,32866268,Luanda,"[""UTC+01:00""]"
|
||||
"Antigua und Barbuda",97928,"Saint John's","[""UTC-04:00""]"
|
||||
Argentinien,45376763,"Buenos Aires","[""UTC-03:00""]"
|
||||
Armenien,2963234,Yerevan,"[""UTC+04:00""]"
|
||||
Aserbaidschan,10110116,Baku,"[""UTC+04:00""]"
|
||||
Australien,25687041,Canberra,"[""UTC+05:00"",""UTC+06:30"",""UTC+07:00"",""UTC+08:00"",""UTC+09:30"",""UTC+10:00"",""UTC+10:30"",""UTC+11:30""]"
|
||||
Bahamas,393248,Nassau,"[""UTC-05:00""]"
|
||||
Bahrain,1701583,Manama,"[""UTC+03:00""]"
|
||||
Bangladesch,164689383,Dhaka,"[""UTC+06:00""]"
|
||||
Barbados,287371,Bridgetown,"[""UTC-04:00""]"
|
||||
Belgien,11555997,Brussels,"[""UTC+01:00""]"
|
||||
Belize,397621,Belmopan,"[""UTC-06:00""]"
|
||||
Benin,12123198,Porto-Novo,"[""UTC+01:00""]"
|
||||
Bhutan,771612,Thimphu,"[""UTC+06:00""]"
|
||||
Bolivien,11673029,Sucre,"[""UTC-04:00""]"
|
||||
"Bosnien und Herzegowina",3280815,Sarajevo,"[""UTC+01:00""]"
|
||||
Botswana,2351625,Gaborone,"[""UTC+02:00""]"
|
||||
Brasilien,212559409,"Brasília","[""UTC-05:00"",""UTC-04:00"",""UTC-03:00"",""UTC-02:00""]"
|
||||
Brunei,437483,"Bandar Seri Begawan","[""UTC+08:00""]"
|
||||
Bulgarien,6927288,Sofia,"[""UTC+02:00""]"
|
||||
"Burkina Faso",20903278,Ouagadougou,"[""UTC""]"
|
||||
Burundi,11890781,Gitega,"[""UTC+02:00""]"
|
||||
Chile,19116209,Santiago,"[""UTC-06:00"",""UTC-04:00""]"
|
||||
China,1402112000,Beijing,"[""UTC+08:00""]"
|
||||
"Costa Rica",5094114,"San José","[""UTC-06:00""]"
|
||||
Deutschland,83240525,Berlin,"[""UTC+01:00""]"
|
||||
Dominica,71991,Roseau,"[""UTC-04:00""]"
|
||||
"Dominikanische Republik",10847904,"Santo Domingo","[""UTC-04:00""]"
|
||||
Dschibuti,988002,Djibouti,"[""UTC+03:00""]"
|
||||
"Dänemark",5831404,Copenhagen,"[""UTC-04:00"",""UTC-03:00"",""UTC-01:00"",""UTC"",""UTC+01:00""]"
|
||||
Ecuador,17643060,Quito,"[""UTC-06:00"",""UTC-05:00""]"
|
||||
"El Salvador",6486201,"San Salvador","[""UTC-06:00""]"
|
||||
"Elfenbeinküste",26378275,Yamoussoukro,"[""UTC""]"
|
||||
Eritrea,5352000,Asmara,"[""UTC+03:00""]"
|
||||
Estland,1331057,Tallinn,"[""UTC+02:00""]"
|
||||
Fidschi,896444,Suva,"[""UTC+12:00""]"
|
||||
Finnland,5530719,Helsinki,"[""UTC+02:00""]"
|
||||
Frankreich,67391582,Paris,"[""UTC-10:00"",""UTC-09:30"",""UTC-09:00"",""UTC-08:00"",""UTC-04:00"",""UTC-03:00"",""UTC+01:00"",""UTC+02:00"",""UTC+03:00"",""UTC+04:00"",""UTC+05:00"",""UTC+10:00"",""UTC+11:00"",""UTC+12:00""]"
|
||||
Gabun,2225728,Libreville,"[""UTC+01:00""]"
|
||||
Gambia,2416664,Banjul,"[""UTC+00:00""]"
|
||||
Georgien,3714000,Tbilisi,"[""UTC+04:00""]"
|
||||
Ghana,31072945,Accra,"[""UTC""]"
|
||||
Grenada,112519,"St. George's","[""UTC-04:00""]"
|
||||
Griechenland,10715549,Athens,"[""UTC+02:00""]"
|
||||
Guatemala,16858333,"Guatemala City","[""UTC-06:00""]"
|
||||
Guinea,13132792,Conakry,"[""UTC""]"
|
||||
Guinea-Bissau,1967998,Bissau,"[""UTC""]"
|
||||
Guyana,786559,Georgetown,"[""UTC-04:00""]"
|
||||
Haiti,11402533,Port-au-Prince,"[""UTC-05:00""]"
|
||||
Honduras,9904608,Tegucigalpa,"[""UTC-06:00""]"
|
||||
Indien,1380004385,"New Delhi","[""UTC+05:30""]"
|
||||
Indonesien,273523621,Jakarta,"[""UTC+07:00"",""UTC+08:00"",""UTC+09:00""]"
|
||||
Irak,40222503,Baghdad,"[""UTC+03:00""]"
|
||||
Iran,83992953,Tehran,"[""UTC+03:30""]"
|
||||
Irland,4994724,Dublin,"[""UTC""]"
|
||||
Island,366425,Reykjavik,"[""UTC""]"
|
||||
Israel,9216900,Jerusalem,"[""UTC+02:00""]"
|
||||
Italien,59554023,Rome,"[""UTC+01:00""]"
|
||||
Jamaika,2961161,Kingston,"[""UTC-05:00""]"
|
||||
Japan,125836021,Tokyo,"[""UTC+09:00""]"
|
||||
Jemen,29825968,"Sana'a","[""UTC+03:00""]"
|
||||
Jordanien,10203140,Amman,"[""UTC+03:00""]"
|
||||
Kambodscha,16718971,"Phnom Penh","[""UTC+07:00""]"
|
||||
Kamerun,26545864,"Yaoundé","[""UTC+01:00""]"
|
||||
Kanada,38005238,Ottawa,"[""UTC-08:00"",""UTC-07:00"",""UTC-06:00"",""UTC-05:00"",""UTC-04:00"",""UTC-03:30""]"
|
||||
"Kap Verde",555988,Praia,"[""UTC-01:00""]"
|
||||
Kasachstan,18754440,Astana,"[""UTC+05:00"",""UTC+06:00""]"
|
||||
Katar,2881060,Doha,"[""UTC+03:00""]"
|
||||
Kenia,53771300,Nairobi,"[""UTC+03:00""]"
|
||||
Kirgisistan,6591600,Bishkek,"[""UTC+06:00""]"
|
||||
Kiribati,119446,"South Tarawa","[""UTC+12:00"",""UTC+13:00"",""UTC+14:00""]"
|
||||
Kolumbien,50882884,"Bogotá","[""UTC-05:00""]"
|
||||
Komoren,869595,Moroni,"[""UTC+03:00""]"
|
||||
Kongo,5657000,Brazzaville,"[""UTC+01:00""]"
|
||||
"Kongo (Dem. Rep.)",108407721,Kinshasa,"[""UTC+01:00"",""UTC+02:00""]"
|
||||
Kosovo,1775378,Pristina,"[""UTC+01:00""]"
|
||||
Kroatien,4047200,Zagreb,"[""UTC+01:00""]"
|
||||
Kuba,11326616,Havana,"[""UTC-05:00""]"
|
||||
Kuwait,4270563,"Kuwait City","[""UTC+03:00""]"
|
||||
Laos,7275556,Vientiane,"[""UTC+07:00""]"
|
||||
Lesotho,2142252,Maseru,"[""UTC+02:00""]"
|
||||
Lettland,1901548,Riga,"[""UTC+02:00""]"
|
||||
Libanon,6825442,Beirut,"[""UTC+02:00""]"
|
||||
Liberia,5057677,Monrovia,"[""UTC""]"
|
||||
Libyen,6871287,Tripoli,"[""UTC+01:00""]"
|
||||
Liechtenstein,38137,Vaduz,"[""UTC+01:00""]"
|
||||
Litauen,2794700,Vilnius,"[""UTC+02:00""]"
|
||||
Luxemburg,632275,Luxembourg,"[""UTC+01:00""]"
|
||||
Madagaskar,27691019,Antananarivo,"[""UTC+03:00""]"
|
||||
Malawi,19129955,Lilongwe,"[""UTC+02:00""]"
|
||||
Malaysia,32365998,"Kuala Lumpur","[""UTC+08:00""]"
|
||||
Malediven,540542,"Malé","[""UTC+05:00""]"
|
||||
Mali,20250834,Bamako,"[""UTC""]"
|
||||
Malta,525285,Valletta,"[""UTC+01:00""]"
|
||||
Marokko,36910558,Rabat,"[""UTC""]"
|
||||
Marshallinseln,59194,Majuro,"[""UTC+12:00""]"
|
||||
Mauretanien,4649660,Nouakchott,"[""UTC""]"
|
||||
Mauritius,1265740,"Port Louis","[""UTC+04:00""]"
|
||||
Mexiko,128932753,"Mexico City","[""UTC-08:00"",""UTC-07:00"",""UTC-06:00""]"
|
||||
Mikronesien,115021,Palikir,"[""UTC+10:00"",""UTC+11:00""]"
|
||||
Moldawien,2617820,"Chișinău","[""UTC+02:00""]"
|
||||
Monaco,39244,Monaco,"[""UTC+01:00""]"
|
||||
Mongolei,3278292,"Ulan Bator","[""UTC+07:00"",""UTC+08:00""]"
|
||||
Montenegro,621718,Podgorica,"[""UTC+01:00""]"
|
||||
Mosambik,31255435,Maputo,"[""UTC+02:00""]"
|
||||
Myanmar,54409794,Naypyidaw,"[""UTC+06:30""]"
|
||||
Namibia,2540916,Windhoek,"[""UTC+01:00""]"
|
||||
Nauru,10834,Yaren,"[""UTC+12:00""]"
|
||||
Nepal,29136808,Kathmandu,"[""UTC+05:45""]"
|
||||
Neuseeland,5084300,Wellington,"[""UTC-11:00"",""UTC-10:00"",""UTC+12:00"",""UTC+12:45"",""UTC+13:00""]"
|
||||
Nicaragua,6624554,Managua,"[""UTC-06:00""]"
|
||||
Niederlande,16655799,Amsterdam,"[""UTC+01:00""]"
|
||||
Niger,24206636,Niamey,"[""UTC+01:00""]"
|
||||
Nigeria,206139587,Abuja,"[""UTC+01:00""]"
|
||||
Nordkorea,25778815,Pyongyang,"[""UTC+09:00""]"
|
||||
Nordmazedonien,2077132,Skopje,"[""UTC+01:00""]"
|
||||
Norwegen,5379475,Oslo,"[""UTC+01:00""]"
|
||||
Oman,5106622,Muscat,"[""UTC+04:00""]"
|
||||
Osttimor,1318442,Dili,"[""UTC+09:00""]"
|
||||
Pakistan,220892331,Islamabad,"[""UTC+05:00""]"
|
||||
Palau,18092,Ngerulmud,"[""UTC+09:00""]"
|
||||
Panama,4314768,"Panama City","[""UTC-05:00""]"
|
||||
Papua-Neuguinea,8947027,"Port Moresby","[""UTC+10:00""]"
|
||||
Paraguay,7132530,"Asunción","[""UTC-04:00""]"
|
||||
Peru,32971846,Lima,"[""UTC-05:00""]"
|
||||
Philippinen,109581085,Manila,"[""UTC+08:00""]"
|
||||
Polen,37950802,Warsaw,"[""UTC+01:00""]"
|
||||
Portugal,10305564,Lisbon,"[""UTC-01:00"",""UTC""]"
|
||||
Ruanda,12952209,Kigali,"[""UTC+02:00""]"
|
||||
"Rumänien",19286123,Bucharest,"[""UTC+02:00""]"
|
||||
Russland,144104080,Moscow,"[""UTC+03:00"",""UTC+04:00"",""UTC+06:00"",""UTC+07:00"",""UTC+08:00"",""UTC+09:00"",""UTC+10:00"",""UTC+11:00"",""UTC+12:00""]"
|
||||
Salomonen,686878,Honiara,"[""UTC+11:00""]"
|
||||
Sambia,18383956,Lusaka,"[""UTC+02:00""]"
|
||||
Samoa,198410,Apia,"[""UTC+13:00""]"
|
||||
"San Marino",33938,"City of San Marino","[""UTC+01:00""]"
|
||||
Saudi-Arabien,34813867,Riyadh,"[""UTC+03:00""]"
|
||||
Schweden,10353442,Stockholm,"[""UTC+01:00""]"
|
||||
Schweiz,8654622,Bern,"[""UTC+01:00""]"
|
||||
Senegal,16743930,Dakar,"[""UTC""]"
|
||||
Serbien,6908224,Belgrade,"[""UTC+01:00""]"
|
||||
Seychellen,98462,Victoria,"[""UTC+04:00""]"
|
||||
"Sierra Leone",7976985,Freetown,"[""UTC""]"
|
||||
Simbabwe,14862927,Harare,"[""UTC+02:00""]"
|
||||
Singapur,5685807,Singapore,"[""UTC+08:00""]"
|
||||
Slowakei,5458827,Bratislava,"[""UTC+01:00""]"
|
||||
Slowenien,2100126,Ljubljana,"[""UTC+01:00""]"
|
||||
Somalia,15893219,Mogadishu,"[""UTC+03:00""]"
|
||||
Spanien,47351567,Madrid,"[""UTC"",""UTC+01:00""]"
|
||||
"Sri Lanka",21919000,"Sri Jayawardenepura Kotte","[""UTC+05:30""]"
|
||||
"St. Kitts und Nevis",53192,Basseterre,"[""UTC-04:00""]"
|
||||
"St. Lucia",183629,Castries,"[""UTC-04:00""]"
|
||||
"St. Vincent und die Grenadinen",110947,Kingstown,"[""UTC-04:00""]"
|
||||
Sudan,43849269,Khartoum,"[""UTC+03:00""]"
|
||||
Suriname,586634,Paramaribo,"[""UTC-03:00""]"
|
||||
Swasiland,1160164,Mbabane,"[""UTC+02:00""]"
|
||||
Syrien,17500657,Damascus,"[""UTC+02:00""]"
|
||||
"São Tomé und Príncipe",219161,"São Tomé","[""UTC""]"
|
||||
"Südafrika",59308690,Pretoria,"[""UTC+02:00""]"
|
||||
"Südkorea",51780579,Seoul,"[""UTC+09:00""]"
|
||||
"Südsudan",11193729,Juba,"[""UTC+03:00""]"
|
||||
Tadschikistan,9537642,Dushanbe,"[""UTC+05:00""]"
|
||||
Tansania,59734213,Dodoma,"[""UTC+03:00""]"
|
||||
Thailand,69799978,Bangkok,"[""UTC+07:00""]"
|
||||
Togo,8278737,"Lomé","[""UTC""]"
|
||||
Tonga,105697,"Nuku'alofa","[""UTC+13:00""]"
|
||||
"Trinidad und Tobago",1399491,"Port of Spain","[""UTC-04:00""]"
|
||||
Tschad,16425859,"N'Djamena","[""UTC+01:00""]"
|
||||
Tschechien,10698896,Prague,"[""UTC+01:00""]"
|
||||
Tunesien,11818618,Tunis,"[""UTC+01:00""]"
|
||||
Turkmenistan,6031187,Ashgabat,"[""UTC+05:00""]"
|
||||
Tuvalu,11792,Funafuti,"[""UTC+12:00""]"
|
||||
"Türkei",84339067,Ankara,"[""UTC+03:00""]"
|
||||
Uganda,45741000,Kampala,"[""UTC+03:00""]"
|
||||
Ukraine,44134693,Kyiv,"[""UTC+02:00""]"
|
||||
Ungarn,9749763,Budapest,"[""UTC+01:00""]"
|
||||
Uruguay,3473727,Montevideo,"[""UTC-03:00""]"
|
||||
Usbekistan,34232050,Tashkent,"[""UTC+05:00""]"
|
||||
Vanuatu,307150,"Port Vila","[""UTC+11:00""]"
|
||||
Vatikanstadt,451,"Vatican City","[""UTC+01:00""]"
|
||||
Venezuela,28435943,Caracas,"[""UTC-04:00""]"
|
||||
"Vereinigte Arabische Emirate",9890400,"Abu Dhabi","[""UTC+04:00""]"
|
||||
"Vereinigte Staaten",329484123,"Washington, D.C.","[""UTC-12:00"",""UTC-11:00"",""UTC-10:00"",""UTC-09:00"",""UTC-08:00"",""UTC-07:00"",""UTC-06:00"",""UTC-05:00"",""UTC-04:00"",""UTC+10:00"",""UTC+12:00""]"
|
||||
"Vereinigtes Königreich",67215293,London,"[""UTC-08:00"",""UTC-05:00"",""UTC-04:00"",""UTC-03:00"",""UTC-02:00"",""UTC"",""UTC+01:00"",""UTC+02:00"",""UTC+06:00""]"
|
||||
Vietnam,97338583,Hanoi,"[""UTC+07:00""]"
|
||||
"Weißrussland",9398861,Minsk,"[""UTC+03:00""]"
|
||||
"Zentralafrikanische Republik",4829764,Bangui,"[""UTC+01:00""]"
|
||||
Zypern,1207361,Nicosia,"[""UTC+02:00""]"
|
||||
"Ägypten",102334403,Cairo,"[""UTC+02:00""]"
|
||||
"Äquatorialguinea",1402985,Malabo,"[""UTC+01:00""]"
|
||||
"Äthiopien",114963583,"Addis Ababa","[""UTC+03:00""]"
|
||||
"Österreich",8917205,Vienna,"[""UTC+01:00""]"
|
||||
|
1
sqlite/countries.json
Normal file
1
sqlite/countries.json
Normal file
File diff suppressed because one or more lines are too long
15
sqlite/countries.sql
Normal file
15
sqlite/countries.sql
Normal file
@ -0,0 +1,15 @@
|
||||
.headers on
|
||||
.mode table
|
||||
|
||||
WITH raw AS (
|
||||
SELECT
|
||||
value AS country
|
||||
FROM json_each(readfile('countries.json'))
|
||||
)
|
||||
SELECT
|
||||
json_extract(country, '$.translations.deu.common') AS german_name,
|
||||
json_extract(country, '$.population') AS population,
|
||||
json_extract(country, '$.capital[0]') AS capital,
|
||||
json_extract(country, '$.timezones') AS timezones
|
||||
FROM raw
|
||||
ORDER BY german_name;
|
||||
18
sqlite/countries_csv.sql
Normal file
18
sqlite/countries_csv.sql
Normal file
@ -0,0 +1,18 @@
|
||||
.mode csv
|
||||
.headers on
|
||||
.output countries.csv
|
||||
|
||||
WITH raw AS (
|
||||
SELECT
|
||||
value AS country
|
||||
FROM json_each(readfile('countries.json'))
|
||||
)
|
||||
SELECT
|
||||
json_extract(country, '$.translations.deu.common') AS german_name,
|
||||
json_extract(country, '$.population') AS population,
|
||||
json_extract(country, '$.capital[0]') AS capital,
|
||||
json_extract(country, '$.timezones') AS timezones
|
||||
FROM raw
|
||||
ORDER BY german_name;
|
||||
|
||||
.output stdout
|
||||
86
sqlite/countries_import.sh
Executable file
86
sqlite/countries_import.sh
Executable file
@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to import REST Countries JSON data into SQLite
|
||||
# Usage: ./import_countries.sh countries.json
|
||||
|
||||
JSON_FILE="${1:-countries.json}"
|
||||
DB_FILE="countries.db"
|
||||
|
||||
# Check if JSON file exists
|
||||
if [ ! -f "$JSON_FILE" ]; then
|
||||
echo "Error: $JSON_FILE not found!"
|
||||
echo "Usage: $0 <path-to-countries.json>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Creating SQLite database: $DB_FILE"
|
||||
|
||||
# Remove existing database if present
|
||||
rm -f "$DB_FILE"
|
||||
|
||||
# Create database and import JSON
|
||||
sqlite3 "$DB_FILE" << 'EOF'
|
||||
-- Create table with JSON column
|
||||
CREATE TABLE countries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
data JSON NOT NULL
|
||||
);
|
||||
|
||||
-- Enable JSON mode for import
|
||||
.mode json
|
||||
|
||||
-- Read and insert JSON data
|
||||
.import countries.json countries_temp
|
||||
|
||||
-- The import creates a temporary table, now we need to move data
|
||||
-- Since the JSON file is an array, we need to handle it properly
|
||||
EOF
|
||||
|
||||
# Alternative approach: Use jq to process JSON array and insert row by row
|
||||
echo "Processing JSON data..."
|
||||
|
||||
sqlite3 "$DB_FILE" << 'EOF'
|
||||
-- If direct import doesn't work, we'll insert via stdin
|
||||
CREATE TABLE IF NOT EXISTS countries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
data JSON NOT NULL
|
||||
);
|
||||
EOF
|
||||
|
||||
# Use jq to split array into individual objects and insert
|
||||
jq -c '.[]' "$JSON_FILE" | while IFS= read -r country; do
|
||||
sqlite3 "$DB_FILE" "INSERT INTO countries (data) VALUES (json('$country'));"
|
||||
done
|
||||
|
||||
echo "Import complete!"
|
||||
echo "Database created: $DB_FILE"
|
||||
echo ""
|
||||
echo "Running sample queries..."
|
||||
echo ""
|
||||
|
||||
# Sample queries
|
||||
sqlite3 -column -header "$DB_FILE" << 'EOF'
|
||||
-- Count total countries
|
||||
SELECT COUNT(*) as total_countries FROM countries;
|
||||
|
||||
-- Get countries with German names, population, and timezones
|
||||
SELECT
|
||||
json_extract(data, '$.name.common') as country_name,
|
||||
json_extract(data, '$.name.nativeName.deu.official') as german_name,
|
||||
json_extract(data, '$.population') as population,
|
||||
json_extract(data, '$.timezones') as timezones
|
||||
FROM countries
|
||||
WHERE json_extract(data, '$.name.nativeName.deu.official') IS NOT NULL
|
||||
LIMIT 10;
|
||||
|
||||
-- Get all German translations (from translations field)
|
||||
SELECT
|
||||
json_extract(data, '$.name.common') as country_name,
|
||||
json_extract(data, '$.translations.deu.official') as german_official,
|
||||
json_extract(data, '$.translations.deu.common') as german_common
|
||||
FROM countries
|
||||
LIMIT 10;
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "Database ready! You can now query with: sqlite3 $DB_FILE"
|
||||
137
sqlite/extract.sql
Normal file
137
sqlite/extract.sql
Normal file
@ -0,0 +1,137 @@
|
||||
-- Attach a new database with the specified name
|
||||
ATTACH DATABASE 'food.db' AS food;
|
||||
ATTACH DATABASE 'import.db' AS src;
|
||||
-- ATTACH DATABASE 'food10k.db.orig' AS src;
|
||||
ATTACH DATABASE ':memory:' AS cache;
|
||||
-- ATTACH DATABASE 'memory.db' AS cache;
|
||||
|
||||
SELECT time(), "Creating tables and temporary tables.";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS food.product (
|
||||
id INTEGER PRIMARY KEY,
|
||||
_id TEXT,
|
||||
name TEXT,
|
||||
created_t INTEGER,
|
||||
last_modified_t INTEGER,
|
||||
countries TEXT,
|
||||
creator_id INTEGER,
|
||||
brand_owner_id INTEGER,
|
||||
nutrition_grades
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS food.creator (
|
||||
creator_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cache.creator (
|
||||
creator_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS food.brand_owner (
|
||||
brand_owner_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
brand_owner TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS food.product_nutriment_temp (
|
||||
product_id INTEGER,
|
||||
nutriment_key TEXT,
|
||||
nutriment_value ANY
|
||||
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS food.product_nutriment (
|
||||
product_id INTEGER,
|
||||
nutriment_id INTEGER,
|
||||
nutriment_value ANY
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS food.nutriment (
|
||||
nutriment_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nutriment TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cache.brand_owner (
|
||||
brand_owner_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
brand_owner TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cache.product (
|
||||
id INTEGER PRIMARY KEY,
|
||||
_id TEXT,
|
||||
name TEXT,
|
||||
created_t INTEGER,
|
||||
last_modified_t INTEGER,
|
||||
countries TEXT,
|
||||
creator TEXT,
|
||||
brand_owner TEXT,
|
||||
nutrition_grades
|
||||
);
|
||||
|
||||
SELECT time(), "Reading and caching base data.";
|
||||
|
||||
INSERT INTO cache.product (id, _id, name, created_t, last_modified_t, countries, creator, brand_owner, nutrition_grades) SELECT ID,
|
||||
json_extract(data, '$._id'), json_extract(data, '$.product_name'),
|
||||
json_extract(data, '$.created_t'), json_extract(data, '$.last_modified_t'),
|
||||
json_extract(data, '$.countries'), json_extract(data, '$.creator'), json_extract(data, '$.brand_owner'),
|
||||
json_extract(data, '$.nutrition_grades')
|
||||
from foodraw;
|
||||
|
||||
|
||||
SELECT time(), "Inserting cached data into creator table.";
|
||||
|
||||
INSERT INTO food.creator (creator )SELECT DISTINCT creator from cache.product ORDER BY creator;
|
||||
INSERT INTO cache.creator (creator )SELECT DISTINCT creator from cache.product ORDER BY creator;
|
||||
|
||||
SELECT time(), "Inserting cached data into brand_owner table.";
|
||||
|
||||
INSERT INTO food.brand_owner (brand_owner )SELECT DISTINCT brand_owner from cache.product ORDER BY brand_owner;
|
||||
INSERT INTO cache.brand_owner (brand_owner )SELECT DISTINCT brand_owner from cache.product ORDER BY brand_owner;
|
||||
|
||||
|
||||
SELECT time(), "Inserting cached data into product table.";
|
||||
|
||||
INSERT INTO food.product (id, _id, name, created_t, last_modified_t, countries, creator_id, brand_owner_id, nutrition_grades)
|
||||
SELECT id, _id, name, created_t, last_modified_t, countries, creator.creator_id as creator_id, brand_owner.brand_owner_id as brand_owner_id, nutrition_grades
|
||||
FROM cache.product
|
||||
LEFT JOIN cache.creator on cache.product.creator = cache.creator.creator
|
||||
LEFT JOIN cache.brand_owner on cache.product.brand_owner = cache.brand_owner.brand_owner
|
||||
where cache.product.name is not null and cache.product.name != "";
|
||||
|
||||
CREATE VIEW food.product_view as
|
||||
SELECT id, _id, name, created_t, last_modified_t, countries, nutrition_grades, brand_owner.brand_owner, creator.creator FROM product
|
||||
LEFT JOIN brand_owner on product.brand_owner_id = brand_owner.brand_owner_id
|
||||
LEFT JOIN creator on product.creator_id = creator.creator_id
|
||||
;
|
||||
|
||||
SELECT time(), "Reading nutriments and writing product_nutriment_temp table.";
|
||||
|
||||
INSERT INTO food.product_nutriment_temp ( product_id, nutriment_key, nutriment_value) select foodraw.id, key, value
|
||||
from foodraw, json_each( data ->> '$.nutriments' );
|
||||
|
||||
INSERT INTO food.nutriment (nutriment) SELECT DISTINCT nutriment_key from food.product_nutriment_temp order by nutriment_key;
|
||||
|
||||
SELECT time(), "Writing product_nutriment table.";
|
||||
|
||||
INSERT INTO food.product_nutriment (product_id, nutriment_id, nutriment_value)
|
||||
SELECT product_id, nutriment_id, nutriment_value from product_nutriment_temp JOIN nutriment on product_nutriment_temp.nutriment_key = nutriment.nutriment
|
||||
WHERE nutriment_value != ""
|
||||
;
|
||||
|
||||
CREATE VIEW food.nutriment_view as
|
||||
SELECT product_id, nutriment, nutriment_value from product_nutriment NATURAL JOIN nutriment;
|
||||
|
||||
DROP TABLE food.product_nutriment_temp;
|
||||
|
||||
CREATE INDEX product_nutriment_product_product_id on product_nutriment(product_id);
|
||||
CREATE INDEX product_nutriment_product_nutriment_id on product_nutriment(nutriment_id);
|
||||
CREATE INDEX product_creator_id on product(creator_id);
|
||||
CREATE INDEX product_brandowner_id on product(brand_owner_id);
|
||||
CREATE INDEX product_name on product(name);
|
||||
|
||||
|
||||
|
||||
DETACH DATABASE food;
|
||||
DETACH DATABASE src;
|
||||
DETACH DATABASE cache;
|
||||
27
sqlite/import.sh
Executable file
27
sqlite/import.sh
Executable file
@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Define the name of the SQLite commands file
|
||||
Imp_SQL_FILE="import_json.sql" # sql file perform the raw import
|
||||
Ext_SQL_FILE="extract.sql" # sql file to extract the data from the imported db
|
||||
|
||||
# Database file names:
|
||||
IMP_DB_FILE="import.db"
|
||||
TGT_DB_FILE="food.db"
|
||||
MEMORY_DB_FILE="memory.db"
|
||||
|
||||
# cleanup from previous runs
|
||||
[ -e $IMP_DB_FILE ] && echo "$(date +"%T")|deleting $IMP_DB_FILE file" && rm $IMP_DB_FILE
|
||||
[ -e $TGT_DB_FILE ] && echo "$(date +"%T")|deleting $TGT_DB_FILE file"&& rm $TGT_DB_FILE
|
||||
[ -e $MEMORY_DB_FILE ] && echo "$(date +"%T")|deleting $TGT_DB_FILE file"&& rm $MEMORY_DB_FILE
|
||||
|
||||
# Execute SQLite commands from the SQL file
|
||||
echo "$(date +"%T")|importing json file started"
|
||||
|
||||
sqlite3 "$IMP_DB_FILE" < "$Imp_SQL_FILE" 2>/dev/null
|
||||
|
||||
echo "$(date +"%T")|importing json file finished"
|
||||
|
||||
# Execute SQLite commands from the SQL file
|
||||
sqlite3 < "$Ext_SQL_FILE"
|
||||
|
||||
echo "$(date +"%T")|creating $TGT_DB_FILE finished"
|
||||
7
sqlite/import_json.sql
Normal file
7
sqlite/import_json.sql
Normal file
@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS foodraw (
|
||||
data JSON NOT NULL,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
);
|
||||
|
||||
.import off1000lines.jsonl foodraw
|
||||
|
||||
1000
sqlite/off1000lines.jsonl
Normal file
1000
sqlite/off1000lines.jsonl
Normal file
File diff suppressed because one or more lines are too long
70
sqlite/sudoku.sql
Normal file
70
sqlite/sudoku.sql
Normal file
@ -0,0 +1,70 @@
|
||||
WITH RECURSIVE
|
||||
input(sud) AS (
|
||||
VALUES('53..7....6..195....98....6.8...6...34..8.3..17...2...6.6....28....419..5....8..79')
|
||||
),
|
||||
|
||||
digits(z, lp) AS (
|
||||
VALUES('1', 1)
|
||||
UNION ALL
|
||||
SELECT CAST(lp+1 AS TEXT), lp+1 FROM digits WHERE lp<9
|
||||
),
|
||||
|
||||
x(s, ind) AS (
|
||||
SELECT sud, instr(sud, '.') FROM input
|
||||
UNION ALL
|
||||
SELECT
|
||||
substr(s, 1, ind-1) || z || substr(s, ind+1),
|
||||
instr(substr(s, 1, ind-1) || z || substr(s, ind+1), '.')
|
||||
FROM x, digits AS z
|
||||
WHERE ind>0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM digits AS lp
|
||||
WHERE z.z = substr(s, ((ind-1)/9)*9 + lp, 1)
|
||||
OR z.z = substr(s, ((ind-1)%9) + (lp-1)*9 + 1, 1)
|
||||
OR z.z = substr(s, (((ind-1)/3) % 3)*3
|
||||
+ ((ind-1)/27)*27
|
||||
+ lp + ((lp-1)/3)*6, 1)
|
||||
)
|
||||
),
|
||||
|
||||
solved(s) AS (
|
||||
SELECT s FROM x WHERE ind=0
|
||||
),
|
||||
|
||||
-- Build 9 pretty rows for both input and solved using digits.lp = 1..9
|
||||
grid_rows(name, rn, line) AS (
|
||||
SELECT 'input', d.lp,
|
||||
substr(sud, (d.lp-1)*9+1, 3)
|
||||
|| ' | ' ||
|
||||
substr(sud, (d.lp-1)*9+4, 3)
|
||||
|| ' | ' ||
|
||||
substr(sud, (d.lp-1)*9+7, 3)
|
||||
FROM input CROSS JOIN digits d
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT 'solved', d.lp,
|
||||
substr(s, (d.lp-1)*9+1, 3)
|
||||
|| ' | ' ||
|
||||
substr(s, (d.lp-1)*9+4, 3)
|
||||
|| ' | ' ||
|
||||
substr(s, (d.lp-1)*9+7, 3)
|
||||
FROM solved CROSS JOIN digits d
|
||||
),
|
||||
|
||||
-- Insert horizontal separators after rows 3 and 6 by computing an ordering key
|
||||
grid_with_sep AS (
|
||||
SELECT name, rn*2-1 AS ord, line FROM grid_rows
|
||||
UNION ALL
|
||||
SELECT name, rn*2 AS ord, '----+-----+----' FROM grid_rows WHERE rn IN (3,6)
|
||||
)
|
||||
|
||||
-- Single final UNIONed selection, ordered by group + position
|
||||
SELECT line FROM (
|
||||
SELECT 1 AS grp, ord AS ord, line FROM grid_with_sep WHERE name='input'
|
||||
UNION ALL
|
||||
SELECT 2 AS grp, 0 AS ord, '' AS line
|
||||
UNION ALL
|
||||
SELECT 3 AS grp, ord AS ord, line FROM grid_with_sep WHERE name='solved'
|
||||
) ORDER BY grp, ord;
|
||||
Loading…
x
Reference in New Issue
Block a user