Skip to content

Commit

Permalink
Improve default value generation
Browse files Browse the repository at this point in the history
The default values are not stored as values in
the schema, but actually as SQL expressions.
So we need to evaluate them, which we do in a
pretty inefficient way, but that shouldn't matter
much for the code generation usecase :-)
  • Loading branch information
helje5 committed Oct 3, 2024
1 parent ad0d8c2 commit 8ef2f23
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// Created by Helge Heß.
// Copyright © 2022 ZeeZide GmbH.
// Copyright © 2022-2024 ZeeZide GmbH.
//

import LighterCodeGenAST
Expand Down Expand Up @@ -89,9 +89,9 @@ extension EnlighterASTGenerator {

case .bool:
switch value {
case "true", "YES", "1" : return .true
case "false", "NO", "0" : return .false
default : return cannotConvert()
case "true", "YES", "1", "TRUE" : return .true
case "false", "NO", "0", "FALSE" : return .false
default : return cannotConvert()
}

case .date:
Expand Down Expand Up @@ -140,6 +140,49 @@ extension EnlighterASTGenerator {
( "uuid", .tuple(value.map { .integer(Int($0)) }) )
])
}
case .currentDate, .currentTime: // YYYY-MM-DD & HH:MM:SS
// those only make sense for string properties?
switch property.propertyType {
case .integer, .double, .decimal, .bool, .date, .url, .uint8Array,
.data, .uuid, .custom:
return cannotConvert()
case .string:
/* TODO: https://github.com/Lighter-swift/Lighter/issues/34
needs to generate this:
var fmt = DateFormatter()
fmt.locale = Locale(identifier: "en_US_POSIX")
fmt.dateFormat = "yyyy-MM-dd" // "HH:mm:ss"
fmt.timeZone = TimeZone(secondsFromGMT: 0)
Date().string(from: Date())
*/
return cannotConvert()
}
case .currentTimestamp:
switch property.propertyType {
case .decimal, .bool, .url, .uint8Array, .data, .uuid, .custom:
return cannotConvert()
case .date:
return .call(name: "Foundation.Date()")
case .double:
return .variableReference(
instance: "Foundation.Date()", name: "timeIntervalSince1970")
case .integer:
return .cast(
.variableReference(
instance: "Foundation.Date()", name: "timeIntervalSince1970"),
to: .int
)
case .string:
/* TODO: https://github.com/Lighter-swift/Lighter/issues/34
needs to generate this:
var fmt = DateFormatter()
fmt.locale = Locale(identifier: "en_US_POSIX")
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
fmt.timeZone = TimeZone(secondsFromGMT: 0)
Date().string(from: Date())
*/
return cannotConvert()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,14 @@ public func generateSwiftInitForSchema(_ schema: Schema,
if let value = column.defaultValue {
source += ", defaultValue: "
switch value {
case .null : source += ".null"
case .integer(let v) : source += ".integer(\(v))"
case .real (let v) : source += ".real(\(v))"
case .text (let v) : source += ".text(\"\(v)\")"
case .blob (let v) : source += ".blob(\(v) /* Not implemented */)"
case .null : source += ".null"
case .integer(let v) : source += ".integer(\(v))"
case .real (let v) : source += ".real(\(v))"
case .text (let v) : source += ".text(\"\(v)\")"
case .blob (let v) : source += ".blob(\(v) /* Not implemented */)"
case .currentDate : source += ".currentDate"
case .currentTime : source += ".currentTime"
case .currentTimestamp : source += ".currentTimestamp"
}
}
if column.isPrimaryKey { source += ", isPrimaryKey: true" }
Expand Down
111 changes: 105 additions & 6 deletions Sources/SQLite3Schema/Column.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ extension Schema {
case real(Double)
case text(String)
case blob([ UInt8 ])

case currentTime
case currentDate
case currentTimestamp
}

/// The internal SQLite3 identifier of the column.
Expand Down Expand Up @@ -79,7 +83,7 @@ extension Schema {
public extension Schema.Column.DefaultValue {

/**
* Returns the ``Schema/TypeAffinity`` of the column.
* Returns the ``Schema/TypeAffinity`` of the columns default value.
*
* In SQLite columns can store any type, even if declared otherwise.
* E.g. you can insert a a TEXT into an INT column, and the TEXT will be
Expand All @@ -98,6 +102,7 @@ public extension Schema.Column.DefaultValue {
case .real : return .real
case .text : return .text
case .blob : return .blob
case .currentDate, .currentTime, .currentTimestamp : return .text
}
}
}
Expand Down Expand Up @@ -142,8 +147,8 @@ public extension Schema.Column {
if rc == SQLITE_DONE { break }
else if rc != SQLITE_ROW { return nil }

if let fkey = Schema.Column(stmt) {
columns.append(fkey)
if let columnInfo = Schema.Column(stmt) {
columns.append(columnInfo)
}
else {
assertionFailure("Could not create foreign key?!")
Expand Down Expand Up @@ -234,7 +239,7 @@ fileprivate extension Schema.Column {

// Distinguish between an explicit NULL (which defaultValue can't store?),
// and just NULL (which then the default fallback is)
let defaultValue = DefaultValue(stmt, 4)
let defaultValue = DefaultValue(stmt, 4, type: type)
self.defaultValue = defaultValue == .null ? nil : defaultValue

isPrimaryKey = sqlite3_column_int64(stmt, 5) != 0
Expand All @@ -250,12 +255,106 @@ fileprivate extension Schema.Column.DefaultValue {
* - stmt: A SQLite API statement handle
* - iCol: The column in the result set, 0-based.
*/
init(_ stmt: OpaquePointer?, _ iCol: Int32) {
init(_ stmt: OpaquePointer?, _ iCol: Int32, type: Schema.ColumnType?) {
guard iCol >= 0 && iCol < sqlite3_column_count(stmt) else {
assertionFailure("Column out of range: \(iCol)")
self = .null
return
}

// This actually contains SQL
// https://www.sqlite.org/lang_createtable.html#the_default_clause
func buildForText(_ text: String, type: Schema.ColumnType?) -> Self {
switch text {
// This happens if the NULL is explicitly specified, like:
// `street VARCHAR DEFAULT NULL`
case "NULL" : return .null
case "CURRENT_TIME" : return .currentTime
case "CURRENT_DATE" : return .currentDate
case "CURRENT_TIMESTAMP" : return .currentTimestamp
default : break
}
// There could be dynamic expressions, like `round(julianday('now'))`,
// but we can't resolve such?

// Some shortcuts for simple constants.
switch type {
case .int, .integer:
if let value = Int64(text) { return .integer(value) }
case .real:
if let value = Double(text) { return .real(value) }
default:
break
}

// This is a "little" inefficient, but an easy starting point.
// TODO: Figure out what to do on issues
let sql = "SELECT \(text);"
var memDBMaybe: OpaquePointer?
guard sqlite3_open_v2(":memory:", &memDBMaybe, SQLITE_OPEN_READONLY,
nil) == SQLITE_OK, let db = memDBMaybe else
{
print("WARN: Could not open in-mem DB for DEFAULT eval.") // eek
return .null
}
defer { sqlite3_close(db) }
var stmt : OpaquePointer?
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
print("WARN: Could not parse DEFAULT value:", text) // eek
return .null
}
defer { sqlite3_finalize(stmt); stmt = nil }

let rc = sqlite3_step(stmt)
guard rc == SQLITE_ROW else {
print("WARN: DEFAULT value produced no row?:", text) // eek
return .null
}

switch sqlite3_column_type(stmt, 0) {
case SQLITE_NULL:
return .null

case SQLITE_INTEGER:
return .integer(sqlite3_column_int64(stmt, 0))

case SQLITE_TEXT:
if let cstr = sqlite3_column_text(stmt, 0) {
return .text(String(cString: cstr))
}
else {
assertionFailure("Unexpected NULL in TEXT affinity default value")
return .null
}

case SQLITE_FLOAT:
return .real(sqlite3_column_double(stmt, 0))

case SQLITE_BLOB:
if let blob = sqlite3_column_blob(stmt, 0) {
let count = Int(sqlite3_column_bytes(stmt, 0))
let buffer = UnsafeRawBufferPointer(start: blob, count: count)
return .blob([UInt8](buffer))
}
else {
assertionFailure("Unexpected NULL in BLOB affinity default value")
return .null
}

default:
if let cstr = sqlite3_column_text(stmt, 0) {
return .text(String(cString: cstr))
}
else {
assertionFailure("Unexpected NULL in TEXT affinity default value")
return .null
}
}
}

// hh(2024-10-03): This always seems to be TEXT or NULL?
// The column does not contain the value, but the SQL expression producing
// a value.
switch sqlite3_column_type(stmt, iCol) {
case SQLITE_NULL:
self = .null
Expand All @@ -265,7 +364,7 @@ fileprivate extension Schema.Column.DefaultValue {

case SQLITE_TEXT:
if let cstr = sqlite3_column_text(stmt, iCol) {
self = .text(String(cString: cstr))
self = buildForText(String(cString: cstr), type: type)
}
else {
assertionFailure("Unexpected NULL in TEXT affinity default value")
Expand Down
17 changes: 11 additions & 6 deletions Tests/ContactsDatabaseTests/contacts-create.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* ZeeQL test schema
*
* Copyright © 2017 ZeeZide GmbH. All rights reserved.
* Copyright © 2017-2024 ZeeZide GmbH. All rights reserved.
*/

CREATE TABLE person (
Expand All @@ -14,13 +14,18 @@ CREATE TABLE person (
CREATE TABLE address (
address_id INTEGER PRIMARY KEY NOT NULL,

street VARCHAR NULL,
city VARCHAR NULL,
state VARCHAR NULL,
country VARCHAR NULL,
street VARCHAR NULL,
city VARCHAR NULL DEFAULT 'Magdeburg',
state VARCHAR NULL,
country VARCHAR NULL,

age INTEGER DEFAULT NULL,
answer INTEGER DEFAULT 42,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,


person_id INTEGER,
FOREIGN KEY(person_id) REFERENCES person(person_id)
FOREIGN KEY(person_id) REFERENCES person(person_id)
ON DELETE CASCADE
DEFERRABLE
);

0 comments on commit 8ef2f23

Please sign in to comment.