Data that describes things that move, seen from places that don't. Eight lessons: seven decisions, then the build. You finish with SkySutra's 22 tables in your schema, sample people and places, and a sign-in that gives every new person a row and a home sky.
How Part 1 works
Turnout's data was typed in by people: clubs, events, RSVPs. SkySutra's is mostly the sky itself, and the sky has habits a database has to respect. Stars have catalog numbers older than any app. The Moon is in a different place every hour. "Tonight" means something different in Tampa and in Tokyo. Each lesson takes one of those habits and turns it into a table decision.
| Lesson | The decision | Tables it shapes |
|---|---|---|
| 1.1 Bodies, stars and constellations | Fixed stars and moving bodies live apart; catalog numbers stay as natural keys | SS_STARS, SS_BODIES, SS_CONSTELLATIONS, SS_CONSTELLATION_LINES |
| 1.2 Nakshatras as data | 27 rows, each pointing at a real star | SS_NAKSHATRAS, SS_SIGNS |
| 1.3 Observers and places | Every person has a home sky from their first sign-in | SS_USERS, SS_PLACES |
| 1.4 Observations and photos | One target per observation; who may see it | SS_OBSERVATIONS, SS_OBSERVATION_PHOTOS |
| 1.5 Time the astronomer's way | Store UTC; key on UTC | Every table with a time |
| 1.6 Store or compute? | Catalog facts are stored; positions are computed | What's not a table |
| 1.7 Sidereal and tropical | The ayanamsa is a dated row | SS_AYANAMSA |
| 1.8 Build the schema | Quick SQL draft, finished by hand; run it | All 22 |
Before you start
You need Part 0 done: a workspace with APEX 26.2 and the empty SkySutra app with its two sign-in schemes (Lessons 0.5 and 0.6). If you skipped it, do 0.5 and 0.6 now; nothing in this part needs more.
Lesson 1.1 — Bodies, stars and constellations
Outcome: explain why the Sun, Moon and planets aren't in the star table, and why a star keeps its catalog number next to its id.
The question. Aldebaran and Mars both shine in the sky tonight. Why do they go in different tables?
The decision. Split them by how they move.
| Fixed stars | Sun, Moon, planets, lunar nodes | |
|---|---|---|
| How many | 9,096 in our catalog | 11 |
| Where they are | A fixed address (RA and Dec), good for centuries | Somewhere new every day |
| Where the position comes from | Stored once, from the catalog | Computed (Part 3) or fetched (Part 4) |
| Table | SS_STARS |
SS_BODIES |
A star row carries its address. A body row carries only who it is: a code (MARS), a name, a Vedic name (Mangala), a kind, and the name the Naval Observatory uses for it (for checking our numbers against theirs). Its position lives in other tables, by place and time, or nowhere at all because it's computed on demand.
The lunar nodes, Rahu and Ketu, are rows in SS_BODIES too, of kind NODE. They aren't objects you can see: they're the two points where the Moon's path crosses the Sun's, which is where eclipses happen. SkySutra shows them and says so (Lesson 0.4's "no predictions" rule applies).
Two keys for a star. Every star in the Yale Bright Star Catalog has an HR number, from the Harvard Revised catalog of 1908: Aldebaran is HR 1457. It's the number astronomers and star atlases use, and the number our constellation-line file uses to say which stars to join. So SS_STARS keeps both:
id, a surrogate key, which every foreign key in SkySutra points at (Turnout Lesson 2.1's rule)hr_number, the natural key, unique, used for loading and matching files (Part 2)
Why not make the HR number the primary key? Because the catalog isn't the only source of stars forever. A fainter star added later, from another catalog, has no HR number. A surrogate id never runs out of meaning.
Constellations are two tables. SS_CONSTELLATIONS holds the 88 official ones: abbreviation (TAU), name (Taurus), Vedic alternate (Vrishabha). The stick figure you see on a chart is a separate table, SS_CONSTELLATION_LINES: one row per line, from one star to another. Taurus is 17 lines. Keeping lines as rows means the sky chart in Part 5 draws every figure with one query.
Check yourselfWhy is Mars in SS_BODIES and not SS_STARS?
Its position changes every day, so it can't have a stored address; stars keep theirs for centuries.
Lesson 1.2 — Nakshatras as data
Outcome: model the 27 nakshatras so the app can show which real star marks each one.
The question. The nakshatras could be a hard-coded list in a PL/SQL package. Why a table?
The decision. Make them data, because the app does more than name them:
| The app needs to | So the row has |
|---|---|
| Find the Moon's nakshatra from its sidereal longitude | start_deg, end_deg: 13°20′ slices from 0 to 360 |
| Show the real star that marks it, on the chart | junction_star_id, a foreign key to SS_STARS |
| Say it in English first, Vedic name alongside | name (Rohini), english_name (the red one) |
| Tell its story (Part 9) | symbol, description, and lore rows that point here |
The foreign key is the point. The junction star (yogatara) of Rohini isn't the text "Aldebaran", it's row HR 1457 in the star table, with a position the app can compute and draw. One nakshatra, one star; a star can mark at most one.
junction_star_id stays empty until Part 2 loads the catalog: there are no stars to point at yet.
The signs are a small table too. SS_SIGNS holds the 12 signs, 30° each, with English and Vedic names (Aries, Mesha). Lesson 9.4 uses it to show why a Western and a Vedic calendar put the Sun in different signs on the same day.
Check yourselfWhy is junction_star_id a foreign key instead of a star name?
So the app can find that star's position and draw it; a name is only text.
Lesson 1.3 — Observers and places
Outcome: design people and places so every person has a sky to look at from their first sign-in.
The question. "What's up tonight?" needs a place. What if a new person hasn't told us theirs?
The decision. Give everyone a home from the start, and let them change it.
SS_USERSis the person: email, display name, theme (Night by default, Lesson 5.1), andhome_place_id.SS_PLACESis anywhere you observe from: name, latitude, longitude, elevation, time zone, and a darkness rating (the Bortle scale, 1 for a truly dark sky, 9 for a city centre).- A place with a
user_idis yours. A place with nouser_idis a shared site anyone can pick: a state park, a dark-sky preserve. - The first time someone signs in, SkySutra creates their user row and a home place: Wesley Chapel, Florida. Part 7 lets them set their own, from the browser's location.
| Choice | Why |
|---|---|
| Each place has its own time zone | "Tonight" in Fort De Soto and in London starts at different instants; the place knows its own clock |
| Home is a row, not a column on the user | Your home and your observing sites are the same kind of thing; queries treat them alike |
home_place_id on the user, plus is_home on the place |
The first is what pages read; the second makes "your home" easy to show in a list |
| A shared site has no owner | One row for the whole community, not a copy per person |
| A point geometry next to latitude and longitude | Forms and web services want the two numbers; distance queries (Part 7) want the point. A trigger keeps them in step |
Who creates the row? Not the pages: a post-authentication procedure, the same idea as Turnout 5.2. APEX proves who someone is; SS_AUTH makes sure SkySutra knows them. Both schemes from Lesson 0.6 call it:
| Scheme | Post-authentication | What it trusts |
|---|---|---|
| Oracle APEX Accounts | ss_auth.post_auth_apex |
The account's username (an email) and its first and last name |
ss_auth.post_auth_google |
Google's email, only if Google says it verified it (Turnout 5.5, "nOAuth") |
Both end in one function, ss_auth.ensure_user(email, name): find the person by email, or create them with a home place. Emails are stored in lower case, because APEX hands them over in capitals (ASHA.RAO@EXAMPLE.COM) and Google in whatever case the person typed. You build it in Lesson 1.8, once the tables exist.
Check yourselfWhy does each place store its own time zone instead of using the person's?
The sky's timing belongs to the place: an observing trip to another time zone has its own "tonight".
Lesson 1.4 — Observations and photos
Outcome: model an observing log where every entry has exactly one target and a clear audience.
The question. "Saw Jupiter's moons", "Saw the Pleiades", "Saw the ISS go over". How does one table hold all three?
The decision. One observation, one target, chosen from four kinds:
| Target | Column |
|---|---|
| A star | star_id |
| A body (Jupiter, the Moon) | body_id |
| A whole constellation (the Pleiades are in Taurus) | constellation_id |
| Anything else (a satellite, a meteor) | other_target, free text |
Exactly one of the four is filled in. That's a rule across four columns, so it's a table-level check constraint:
constraint ss_obs_one_target_ck check (
nvl2(star_id,1,0) + nvl2(body_id,1,0) + nvl2(constellation_id,1,0) + nvl2(other_target,1,0) = 1)
nvl2(x, 1, 0) is 1 when x has a value and 0 when it doesn't; the four must add up to exactly 1.
Who sees it. Two columns, because they answer different questions:
visibility: who the owner wants to see it.PRIVATE(only me),SHARED(people with the link),PUBLIC(everyone, once approved).status: where it is in moderation.DRAFT,IN_REVIEW,PUBLISHED,RETURNED. It moves only through the workflow in Part 8, never by a page updating the column.
Photos are their own table. One observation can have several photos, so SS_OBSERVATION_PHOTOS has one row each: the image as a BLOB, its MIME type, file name and caption. Deleting an observation deletes its photos (on delete cascade): a photo means nothing without its observation. The other cascades pass the same test: a place's cached rise and set times and ISS passes go with the place, and a person's alert settings go with the person. Observations never cascade, so deleting a place someone logged from fails instead of quietly erasing their log. Lesson 8.6 shows how to move the images to Oracle Cloud Object Storage instead, if you have an account.
Check yourselfWhy are visibility and status two columns and not one?
Visibility is the owner's wish; status is the moderator's decision. A public observation can still be in review.
Lesson 1.5 — Time the astronomer's way
Outcome: store every time so it means one instant, and know the one place Oracle makes that awkward.
The question. The ISS passes over Wesley Chapel at 6:54 am on October 31. What exactly goes in the column?
The decision. Three rules.
- Store instants as
TIMESTAMP WITH TIME ZONE, in UTC.2026-10-31 10:54:29 UTCmeans one moment everywhere. Clock changes, travel and servers in other zones can't shift it. - Convert to local time last, on the way to the screen, using the place's time zone:
starts_at at time zone p.time_zone. On November 1 the clocks change and the display shifts an hour; the stored value doesn't move. - Calculations use Julian dates, a single count of days since 4713 BC. Astronomers use them because subtracting two of them gives days, with no months, leap years or zones in the way. Part 3's
ss_time.julian_dateturns a timestamp into one.
The awkward place: keys. Oracle won't let a TIMESTAMP WITH TIME ZONE column be part of a primary key or a unique constraint. Try it, and you get:
ORA-02329: column of datatype TIME/TIMESTAMP WITH TIME ZONE cannot be unique or a primary key
This bit SkySutra's first schema run: four tables quietly failed to create. Two ways round it, and SkySutra uses both:
| Where | What | Example |
|---|---|---|
| A time that is the key | A plain TIMESTAMP that always holds UTC, with the column name saying so |
ss_moon_phases.at_utc, ss_space_weather.time_tag_utc |
| A time that's part of a unique rule | Keep the time zone, and put the unique index on its UTC value | create unique index ss_iss_uk on ss_iss_passes (place_id, sys_extract_utc(starts_at)) |
sys_extract_utc turns a timestamp with time zone into a plain UTC timestamp, which Oracle is happy to index. The second way is a function-based unique index, the same trick Turnout used for "one owner per club".
Local dates are different. "Sunrise on October 23 in Wesley Chapel" is about a calendar day at a place, not an instant, so ss_rise_set.local_date is a plain DATE, and the rise and set times in that row are instants with time zones.
Check yourselfWhy does ss_moon_phases key on a plain TIMESTAMP named at_utc?
Oracle won't make a time-zone timestamp a primary key, so the key is a plain timestamp that always holds UTC, and the name says so.
Lesson 1.6 — Store or compute?
Outcome: decide, for any number SkySutra shows, whether it belongs in a table.
The question. Aldebaran's altitude over Wesley Chapel is a number. Why isn't there a column for it?
The decision. Store what's true for everyone and for a long time; compute what depends on where and when.
| Fact | Stored or computed | Why |
|---|---|---|
| A star's RA, Dec, magnitude, color | Stored, in SS_STARS |
The same for everyone for centuries |
| A star's altitude and azimuth | Computed (ss_coord.star_altitude) |
Different for every place and every minute: storing it would mean 9,096 stars × every place × every minute |
| The Moon's phase, tithi and nakshatra | Computed (ss_sun_moon, ss_vedic) |
Changes by the hour; cheap to work out |
| Rise and set times for each place and day | Stored (SS_RISE_SET) |
Expensive to work out (about 18 seconds a place for a week of the Sun, Moon and planets), asked for constantly; worked out once a night |
| ISS passes over each place | Stored (SS_ISS_PASSES) |
Needs the latest orbit, and alerts must be planned ahead |
| A planet's position right now | Computed (ss_planets, Lesson 3.6) |
As quick as the Moon's |
| Where each body will be through tonight, hour by hour | Stored (SS_BODY_POSITIONS) |
The sky chart and alerts read the night ahead again and again; worked out once a night (Part 10) |
So the rule has a second half: compute by default, store when it's slow or it comes from outside. Stored results are a cache, refreshed on a schedule (Part 10), and every one of those tables has a fetched_at or computed_at column so a page can say how fresh it is (Lesson 4.5).
Check yourselfRise and set times can be computed, so why are they stored?
Working them out takes seconds per place, and every Tonight page asks for them, so they're computed once a night and read many times.
Lesson 1.7 — Sidereal and tropical
Outcome: store the ayanamsa so the app can work out any date's value, and change systems without changing code.
The question. The ayanamsa is about 24°. Why not a constant in the package?
The decision. It's a dated value, so it's a row:
| Column | Example | Meaning |
|---|---|---|
system |
LAHIRI |
Which definition (others exist: Raman, Krishnamurti) |
epoch |
2000-01-01 | The date the value is given for |
value_deg |
23.853 | The ayanamsa on that date, in degrees (23°51′11″) |
rate_arcsec_per_year |
50.29 | How fast it grows: precession, about 1° every 72 years |
The value on any date is value_deg + rate × years since epoch. Lesson 0.4 quoted 24°13′39″ for October 23, 2026: that's this row at work, within 22″ of published tables.
Three reasons for a row instead of a constant:
- It changes with time. A constant is right for one day.
- Traditions differ. A second system is one more row, and the function takes the system as a parameter, defaulting to Lahiri.
- It can be corrected. If a better published value comes along, it's an update, not a code release.
The primary key is (system, epoch), so a system can have more than one dated value, and the function uses the latest one.
Check yourselfHow would you add the Raman ayanamsa?
Insert one row with system RAMAN and its epoch, value and rate; no code changes.
Lesson 1.8 — Build the schema
Outcome: SkySutra's 22 tables, sample data, and a sign-in that gives every new person a row and a home sky.
The question. We could type all 22 tables from scratch, or let Quick SQL write them. Which?
The decision. Both, as in Turnout 2.9: sketch in Quick SQL to see the shape, then run the hand-finished script, and know exactly what the hand added.
1. Sketch it in Quick SQL
Download skysutra_quicksql.txt from this part's downloads. In SQL Workshop › Utilities › Quick SQL, delete the sample text on the left and paste the file. The SQL appears on the right as you type. Don't run it.
What came out on our run, compared with the finished script:
| Decision | Quick SQL draft | skysutra_schema.sql |
|---|---|---|
Identity keys, SS_ prefix |
generated by default on null as identity |
generated always as identity: nobody picks their own id |
| Text lengths in characters | "semantics": "CHAR", so vc20 is varchar2(20 char) |
alter session set nls_length_semantics = char at the top: a Greek letter is two bytes, so "β Trianguli Australis" needs 22 bytes but only 21 characters (Part 2 found this the hard way) |
| Child tables get their foreign key | Yes, from indenting (stars under constellations) |
Same |
| Unique and simple list checks | Yes, but unique names lose the prefix (stars_hr_number_unq) |
Named consistently (ss_stars_hr_uk) |
is_home vc1 /check Y, N |
Turned into a BOOLEAN column (23ai guesses from the is_ prefix), still with the 'Y','N' check |
varchar2(1) with a Y/N check, like Turnout |
| Ranges: RA 0–360, Dec −90–90, magnitude −2–8, latitude, longitude, Bortle 1–9 | None | Check constraints (SQL domains need a privilege apex.oracle.com doesn't grant) |
| Exactly one observation target | No: a rule across four columns | ss_obs_one_target_ck |
created_at |
not null, no default |
default systimestamp |
| Photos go when the observation goes | No cascade | on delete cascade |
| Point geometry in step with latitude and longitude | No | A trigger, ss_places_geom_trg |
| Keys on UTC times | Not in the draft (it has no live-data tables) | Plain TIMESTAMP keys and a function-based unique index (Lesson 1.5) |
Quick SQL is fast for the shape; the rules are still ours.
2. Run the finished scripts
Download the three scripts from this part's downloads. In SQL Workshop › SQL Scripts › Upload, upload each one, then Run it, in this order. Each should finish with no errors.
| Script | What it makes |
|---|---|
skysutra_schema.sql |
The 22 tables, their indexes, and the places trigger |
skysutra_seed.sql |
The Sun, Moon, planets and nodes; the 12 signs; the Lahiri ayanamsa; the 27 nakshatras; Asha, Ben and Maya with their homes; four shared dark-sky sites; four sample observations |
skysutra_auth.sql |
The SS_AUTH package |
Both scripts are safe to run again. skysutra_schema.sql makes each table and index only if it's missing (create table if not exists), so a second run changes nothing. The seed adds a row only if it isn't there already, matched on its natural key: a body's code, a sign's number, a person's email. It never deletes. Your own people, places, observations and photos stay, and so do the junction stars Part 2 sets on the nakshatras. The four sample observations go in only while the log is empty.
3. Connect sign-in
- Shared Components › Authentication Schemes, open Oracle APEX Accounts. Under Login Processing, set Post-Authentication Procedure Name to
ss_auth.post_auth_apex. Apply Changes. - Open Google, set Post-Authentication Procedure Name to
ss_auth.post_auth_google. Apply Changes.
Check: in SQL Commands,
select table_name from user_tables where table_name like 'SS\_%' escape '\' order by 1;
returns 22 rows. Then:
select (select count(*) from ss_bodies) bodies,
(select count(*) from ss_nakshatras) nakshatras,
(select count(*) from ss_users) users,
(select count(*) from ss_places) places,
(select count(*) from ss_observations) observations
from dual;
gives 11, 27, 3, 7 and 4. Now sign in to SkySutra as someone new: create one more APEX end user for yourself (Lesson 0.6, step 1), sign in as them, then run
select u.email, u.display_name, p.name home, p.time_zone
from ss_users u join ss_places p on p.id = u.home_place_id
order by u.id desc fetch first 1 row only;
Your new person is there, in lower case, at home in Wesley Chapel. If you left the account's first and last name empty, the display name is the part of the email before the @ (gabriela for gabriela@example.com); fill them in and it's the full name. Sign in as asha.rao@example.com and nothing new appears: she was already in the seed.
Check yourselfWhy does SkySutra use generated always as identity instead of Quick SQL's by default on null?
With "always", no insert can choose its own id, so ids can't collide with ones the database hands out.
Checkpoint
After this part, SkySutra has its data model and knows who's signed in. The tables are mostly empty on purpose: the stars arrive in Part 2, the sky math in Part 3, the live data in Part 4.
Test it
- The 22 tables exist and the counts above match.
- A new APEX account signs in and gets an
SS_USERSrow and a Wesley Chapel home. - In SQL Commands, try to break a rule, and read the error:
insert into ss_observations (user_id, place_id, observed_at, body_id, other_target)
select id, home_place_id, systimestamp, (select id from ss_bodies where code = 'MOON'), 'Moon and a plane'
from ss_users where email = 'asha.rao@example.com';
It fails with ORA-02290: check constraint (…SS_OBS_ONE_TARGET_CK) violated: two targets at once.
Files
| File | What it is |
|---|---|
skysutra_quicksql.txt |
The Quick SQL first draft (Lesson 1.8) |
skysutra_schema.sql |
The 22 tables, indexes and trigger |
skysutra_seed.sql |
Reference data and sample people, places and observations |
skysutra_auth.sql |
SS_AUTH: the post-authentication procedures |
Exercises
- Add a shared observing site you know, with its own time zone and Bortle rating, as an
insertintoSS_PLACESwith nouser_id. Check that the trigger filledgeom. - Add the Raman ayanamsa (Lesson 1.7) as a second row. Look up its value for 2000-01-01 in a published table.
- Write a query that lists each nakshatra with its slice in degrees and minutes (13°20′ is 13.333…°):
floor(start_deg)andround(mod(start_deg, 1) * 60). - Stretch: try creating a table with a
TIMESTAMP WITH TIME ZONEprimary key, read the ORA-02329, then fix it both ways from Lesson 1.5.
Review questions
Try each one before you open it. The graded quiz is at the end of the page.
1Why are planets in SS_BODIES and not SS_STARS?
Their positions change daily, so they have no stored address
2What is an HR number?
A star's number in the Harvard Revised catalog, kept as a natural key
3Why is junction_star_id a foreign key?
So the app can find and draw the real star, not just name it
4Where does a new person's home come from?
The post-authentication procedure creates one in Wesley Chapel
5How does the schema make sure an observation has exactly one target?
A check constraint that adds up nvl2() of the four target columns and requires 1
6What does ORA-02329 tell you?
A TIMESTAMP WITH TIME ZONE column can't be in a primary key or unique constraint
7Why are rise and set times stored when star altitudes aren't?
They're slow to compute and read constantly; altitudes are quick and change every minute
8Why is the ayanamsa a row?
It changes with time and between traditions, so it's data with a date, not a constant
9What did Quick SQL do with is_home vc1 /check Y, N on our run?
Made it a BOOLEAN column, still with the Y/N check
Downloads for Part 1
Everything this part uses. The SQL files are plain text: open one in any editor before you run it.
- Part 1 kitskysutra-part-1.zipEvery file above in one zip, with a README
- Tablesskysutra_schema.sqlLesson 1.8: the 22 tables, indexes and trigger
- Sample dataskysutra_seed.sqlLesson 1.8: reference data and sample people, places and observations
- Quick SQL draftskysutra_quicksql.txtLesson 1.8: the first draft, to paste into Quick SQL
- Sign-inskysutra_auth.sqlSS_AUTH: the post-authentication procedures
Part 1 quiz
5 questions. Get 4 right to pass. You can retake it as often as you like.